Zorgan
Zorgan

Reputation: 9123

willSet not being called on SwiftUI's new @AppStorage property

I am utilising SwfitUI's new @AppStorage property. However, the willSet property observer is not firing.

Here is my UserDefaults value codeResult changing from an asynchronous call:

class Main: ObservableObject {
    func code(data: [AnyHashable:Any?]){
        UserDefaults.standard.set("incorrect", forKey: "codeResult")
    }
}

And below in my View, willSet is not being called:

struct MainView: View {
    @ObservedObject var main = Main()
    @ObservedObject var mainCountdown: CountDown
    @ObservedObject var locationManager = LocationManager()
    @State private var keyboardHeight: CGFloat = 0
    @State var showCode: Bool = false
    @AppStorage("codeResult") var codeResult = UserDefaults.standard.string(forKey: "codeResult") ?? "" {
        willSet {
            print("willSet: \(newValue)") // doesn't fire 
        }
        didSet {
            print("didSet: \(oldValue)") // doesn't fire
        }
    }

Any idea why it's not firing?

Upvotes: 4

Views: 1611

Answers (2)

Zorgan
Zorgan

Reputation: 9123

Adding onReceive() to the view seemed to successfully track changes to codeResult where I could execute code blocks:

    .onReceive(self.codeResult.publisher.collect()) {_ in
        if self.codeResult == CodeResult.incorrect.rawValue {
            print("CHANGED TO INCORRECT")
            self.showCode = false
        }
    }

Upvotes: 0

Asperi
Asperi

Reputation: 257749

The willSet/didSet are observers for property change (not publisher observers). Thus to get into willSet/didSet you have to change property codeResult directly.

Here is a demo that depicts differences - both buttons update Text (because codeResult is dynamic observable property, but only direct assign to codeResult activates property willSet/didSet)

Tested with Xcode 12b3 / iOS 14.

struct DemoAppStorageDidSet: View {
    @AppStorage("codeResult") var codeResult: String = "Default" { // << default value
        willSet {
            print("willSet: \(newValue)")
        }
        didSet {
            print("didSet: \(oldValue)")
        }
    }

    var body: some View {
        VStack {
            Text("Code Result: \(codeResult)")
            Divider()
            Button("AppStore Update") {
                self.codeResult = "AppStore"   // << activates willSet/didSet !!
            }
            Divider()
            Button("UserDefaults Update") {
                UserDefaults.standard.set("UserDefaults", forKey: "codeResult")
            }
        }
    }
}

Note: you don't need to initialise AppStorage with UserDefaults value, it is read by default.

Upvotes: 2

Related Questions