Search code examples
iosswiftkey-value-observingproperty-wrapper

Implementing UserDefaults using properyWrapper causes weird problem


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?


Solution

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