试图观察 UserDefaults 中的字符串,但遇到编译错误

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

a small project 在 GitHub。

TopViewModel.swift中,我首先获取一个JSON对象列表,然后将它们存储在核心数据中,最后将它们显示在SwiftUI[=12中=].

这很好用,但现在我在顶部添加了一个 Picker,允许用户 select 其中一种语言:“en”、“de”、“ru”和然后将字符串存储在 @AppStorage:

这也很好用,这对我这个 Swift 新手来说还不错 :-)

然而,当我试图从我的视图模型观察 UserDefaults 中的 language 键时,我的问题就开始了:

我尝试将以下代码添加到 TopViewModel.swift,但它甚至无法编译:

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)
}

其中一个编译错误是我的视图模型不是 NSObject

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

为什么不呢?

更新:

我已将 NSObject 添加为 [TopViewModel.swift] 的父对象,现在当用户 select 是语言中的值时调用回调方法 observeValue 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)
    }

剩下的唯一(我认为是次要的)问题是我不知道如何从 observeValue 参数中获取 language 字符串(调用 UserDefaults.standard.string(forKey: "language") 作为解决方法有效,但我有兴趣从参数中提取值,因为调试器在那里显示 language 字符串):

你可以试试这个。

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.observe(\.language, options: [.new], changeHandler: { (defaults, change) in
            // your change logic here
        })
    }
    
    deinit {
        observer?.invalidate()
    }
}

更新

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()

这是我自己的解决方案,我必须添加 NSObject 作为父 class 并使用 change[.newKey]observeValue 回调方法中访问新值:

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"
        // use the language string value here
    }
    
    deinit {
        UserDefaults.standard.removeObserver(self, forKeyPath: "language")
    }
    
    override func observeValue(forKeyPath keyPath: String?,
                      of object: Any?,
                      change: [NSKeyValueChangeKey : Any]?,
                      context: UnsafeMutableRawPointer?) {
        guard keyPath == "language",
              let change = change,
              let language = change[.newKey] as? String else {
            return
        }

        // use the language string value here
    }