Search code examples
iosswiftkey-value-observinguserdefaults

Trying to observe a string in UserDefaults, but struggling with compile errors


I have a small project at GitHub.

In the TopViewModel.swift I first fetch a JSON list of object, then store them in Core Data and finally display them in a SwiftUI List.

This works well, but now I have added a Picker at the top, allowing the user to select one of the languages: "en", "de", "ru" and then store the string in @AppStorage:

screenshot

This also works well, which is not bad for me as a Swift newbie :-)

However my problems begin when I am trying to observe the language key in UserDefaults from my view model:

xcode

I have tried adding the following code to the TopViewModel.swift, but it does not even compile:

init() {
    UserDefaults.standard.addObserver(self, forKeyPath: "language", options: NSKeyValueObservingOptions.new, context: nil)
}

deinit() {
    UserDefaults.standard.removeObserver(self, forKeyPath: "language")
}

func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    // How to get language here from the params?
    updateTopEntities(language: language)
    fetchTopModels(language: language)
}

One of the compile errors is that my view model is not a NSObject

Cannot convert value of type 'TopViewModel' to expected argument type 'NSObject'

why not?

UPDATE:

I have added NSObject as a parent to the [TopViewModel.swift] and now the callback method observeValue is called when the user selects a value in the language Picker:

class TopViewModel: NSObject, ObservableObject {

    override init() {
        super.init()
        UserDefaults.standard.addObserver(self,
                                          forKeyPath: "language",
                                          options: NSKeyValueObservingOptions.new, context: nil)

        let language = UserDefaults.standard.string(forKey: "language") ?? "en"
        updateTopEntities(language: language)
        fetchTopModels(language: language)
    }
    
    deinit {
        UserDefaults.standard.removeObserver(self, forKeyPath: "language")
    }
    
    override func observeValue(forKeyPath keyPath: String?,
                      of object: Any?,
                      change: [NSKeyValueChangeKey : Any]?,
                      context: UnsafeMutableRawPointer?) {
        guard keyPath == "language" else { return }
        guard change?.count == 2 else { return }
        print("observeValue language=\(change["new"].value)")
        // How to get language here from the params?
        //updateTopEntities(language: language)
        //fetchTopModels(language: language)
    }

The only (and I believe minor) problem left is that I don't know how to get the language string from the observeValue params (calling UserDefaults.standard.string(forKey: "language") as a workaround works, but I am interested in extracting the value from the params, because debugger shows the language string there):

xcode 2


Solution

  • You can try this.

    extension UserDefaults {
        @objc dynamic var language: String {
            get { self.string(forKey: "language") ?? "en" }
            set { self.setValue(newValue, forKey: "language") }
        }
    }
    
    class MyObject {
        var observer: NSKeyValueObservation?
        
        init() {
            observer = UserDefaults.standard.observer(\.language, options: [.new], changeHandler: { (defaults, change) in
                // your change logic here
            })
        }
        
        deinit {
            observer?.invalidate()
        }
    }
    

    UPDATE

    import Foundation
    
    extension UserDefaults {
        @objc dynamic var language: String {
            get { self.string(forKey: #function) ?? "en" }
            set { self.setValue(newValue, forKey: #function) }
        }
    }
    
    class TopViewModel: NSObject {
        let defaults = UserDefaults.standard
        let languageKeyPath = #keyPath(UserDefaults.language)
        
        override init() {
            super.init()
            defaults.addObserver(self, forKeyPath: languageKeyPath, options: .new, context: nil)
            
            let language = defaults.language
            print("initialLanguage: \(language)")
            
            defaults.language = "en"
            defaults.language = "fr"
        }
        
        deinit {
            defaults.removeObserver(self, forKeyPath: languageKeyPath)
        }
        
        override func observeValue(forKeyPath keyPath: String?,
                                   of object: Any?,
                                   change: [NSKeyValueChangeKey: Any]?,
                                   context: UnsafeMutableRawPointer?) {
            guard (object as? UserDefaults) === defaults,
                  keyPath == languageKeyPath, 
                  let change = change 
            else { return }
            
            if let updatedLanguage = change[.newKey] as? String {
                print("updatedLanguage : \(updatedLanguage)")
            }
        }
    }
    
    // Test code, run init to observe changes
    let viewModel = TopViewModel()