I am trying to implement UserDefaults class using @propertyWrapper
. What I am trying to do is creating a wrapper class for my app user preferences. So I wrote following code.
@propertyWrapper
struct Storage<T> {
private let key: String
private let defaultValue: T
var projectedValue: Storage<T> { return self }
var wrappedValue: T {
get {
return UserDefaults.standard.string(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
func observe(change: @escaping (T?, T?) -> Void) -> NSObject {
return DefaultsObservation(key: key) { old, new in
change(old as? T, new as? T)
}
}
}
Then, I'd like to observe UserDefaults value changes. So I implemented an observation class named DefaultsObservation
.
class DefaultsObservation: NSObject {
let key: String
private var onChange: (Any, Any) -> Void
init(key: String, onChange: @escaping (Any, Any) -> Void) {
self.onChange = onChange
self.key = key
super.init()
UserDefaults.standard.addObserver(self, forKeyPath: key, options: [.old, .new], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let change = change, object != nil, keyPath == key else { return }
onChange(change[.oldKey] as Any, change[.newKey] as Any)
}
deinit {
UserDefaults.standard.removeObserver(self, forKeyPath: key, context: nil)
}
}
Also my AppData class is following.
struct AppData {
@Storage(key: "layout_key", defaultValue: "list")
static var layout: String
}
However, when I tried to add and listen changes layout
property it is not working properly.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
AppData.$layout.observe { old, new in
print(old)
}
}
When I debugged it, deinit
is working as soon as viewWillAppear
method invoked. When I commented out deinit
method for removing observer everything is working perfect. I think closing deinit
can cause some memory problems. So I do not want to commented it out. What am I missing and how can I solve it?
Method observe(change: @escaping (T?, T?)
initialise DefaultsObservation
object and returns the value. There isn't any strong reference to this object so that's why it's deallocated and deInit
is being called. You need to keep a strong reference of this object. For Example
var valueObserver: NSObject? = nil
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
valueObserver = AppData.$layout.observe { old, new in
print(old)
}
}