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