How to use KVO for UserDefaults in Swift?

前端 未结 4 1548
耶瑟儿~
耶瑟儿~ 2021-02-04 01:00

I\'m rewriting parts of an app, and found this code:

fileprivate let defaults = UserDefaults.standard

func storeValue(_ value: AnyObject, forKey key:String) {
          


        
相关标签:
4条回答
  • 2021-02-04 01:31

    For anyone who will be looking for the answer in the future, didChangeNotification will be posted only if changes are made on the same process, if you would like to receive all updates regardless of the process use KVO.

    Apple doc

    This notification isn't posted when changes are made outside the current process, or when ubiquitous defaults change. You can use key-value observing to register observers for specific keys of interest in order to be notified of all updates, regardless of whether changes are made within or outside the current process.

    Here is a link to demo Xcode project which shows how to setup block based KVO on UserDefaults.

    0 讨论(0)
  • 2021-02-04 01:48

    Swift 4 version made with reusable types:

    File: KeyValueObserver.swift - General purpose reusable KVO observer (for cases where pure Swift observables can't be used).

    public final class KeyValueObserver<ValueType: Any>: NSObject, Observable {
    
       public typealias ChangeCallback = (KeyValueObserverResult<ValueType>) -> Void
    
       private var context = 0 // Value don't reaaly matter. Only address is important.
       private var object: NSObject
       private var keyPath: String
       private var callback: ChangeCallback
    
       public var isSuspended = false
    
       public init(object: NSObject, keyPath: String, options: NSKeyValueObservingOptions = .new,
                   callback: @escaping ChangeCallback) {
          self.object = object
          self.keyPath = keyPath
          self.callback = callback
          super.init()
          object.addObserver(self, forKeyPath: keyPath, options: options, context: &context)
       }
    
       deinit {
          dispose()
       }
    
       public func dispose() {
          object.removeObserver(self, forKeyPath: keyPath, context: &context)
       }
    
       public static func observeNew<T>(object: NSObject, keyPath: String,
          callback: @escaping (T) -> Void) -> Observable {
          let observer = KeyValueObserver<T>(object: object, keyPath: keyPath, options: .new) { result in
             if let value = result.valueNew {
                callback(value)
             }
          }
          return observer
       }
    
       public override func observeValue(forKeyPath keyPath: String?, of object: Any?,
                                         change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
          if context == &self.context && keyPath == self.keyPath {
             if !isSuspended, let change = change, let result = KeyValueObserverResult<ValueType>(change: change) {
                callback(result)
             }
          } else {
             super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
          }
       }
    }
    

    File: KeyValueObserverResult.swift – Helper type to keep KVO observation data.

    public struct KeyValueObserverResult<T: Any> {
    
       public private(set) var change: [NSKeyValueChangeKey: Any]
    
       public private(set) var kind: NSKeyValueChange
    
       init?(change: [NSKeyValueChangeKey: Any]) {
          self.change = change
          guard
             let changeKindNumberValue = change[.kindKey] as? NSNumber,
             let changeKindEnumValue = NSKeyValueChange(rawValue: changeKindNumberValue.uintValue) else {
                return nil
          }
          kind = changeKindEnumValue
       }
    
       // MARK: -
    
       public var valueNew: T? {
          return change[.newKey] as? T
       }
    
       public var valueOld: T? {
          return change[.oldKey] as? T
       }
    
       var isPrior: Bool {
          return (change[.notificationIsPriorKey] as? NSNumber)?.boolValue ?? false
       }
    
       var indexes: NSIndexSet? {
          return change[.indexesKey] as? NSIndexSet
       }
    }
    

    File: Observable.swift - Propocol to suspend/resume and dispose observer.

    public protocol Observable {
       var isSuspended: Bool { get set }
       func dispose()
    }
    
    extension Array where Element == Observable {
    
       public func suspend() {
          forEach {
             var observer = $0
             observer.isSuspended = true
          }
       }
    
       public func resume() {
          forEach {
             var observer = $0
             observer.isSuspended = false
          }
       }
    }
    

    File: UserDefaults.swift - Convenience extension to user defaults.

    extension UserDefaults {
    
       public func observe<T: Any>(key: String, callback: @escaping (T) -> Void) -> Observable {
          let result = KeyValueObserver<T>.observeNew(object: self, keyPath: key) {
             callback($0)
          }
          return result
       }
    
       public func observeString(key: String, callback: @escaping (String) -> Void) -> Observable {
          return observe(key: key, callback: callback)
       }
    
    }
    

    Usage:

    class MyClass {
    
        private var observables: [Observable] = []
    
        // IMPORTANT: DON'T use DOT `.` in key.
        // DOT `.` used to define `KeyPath` and this is what we don't need here.
        private let key = "app-some:test_key"
    
        func setupHandlers() {
           observables.append(UserDefaults.standard.observeString(key: key) {
              print($0) // Will print `AAA` and then `BBB`.
           })
        }
    
        func doSomething() {
           UserDefaults.standard.set("AAA", forKey: key)
           UserDefaults.standard.set("BBB", forKey: key)
        }
    }
    

    Updating defaults from Command line:

    # Running shell command below while sample code above is running will print `CCC`
    defaults write com.my.bundleID app-some:test_key CCC
    
    0 讨论(0)
  • 2021-02-04 01:49

    As of iOS 11 + Swift 4, the recommended way (according to SwiftLint) is using the block-based KVO API.

    Example:

    Let's say I have an integer value stored in my user defaults and it's called greetingsCount.

    First I need to extend UserDefaults:

    extension UserDefaults {
        @objc dynamic var greetingsCount: Int {
            return integer(forKey: "greetingsCount")
        }
    }
    

    This allows us to later on define the key path for observing, like this:

    var observer: NSKeyValueObservation?
    
    init() {
        observer = UserDefaults.standard.observe(\.greetingsCount, options: [.initial, .new], changeHandler: { (defaults, change) in
            // your change logic here
        })
    }
    

    And never forget to clean up:

    deinit {
        observer?.invalidate()
    }
    
    0 讨论(0)
  • 2021-02-04 01:50

    From the blog of David Smith http://dscoder.com/defaults.html https://twitter.com/catfish_man/status/674727133017587712

    If one process sets a shared default, then notifies another process to read it, then you may be in one of the very few remaining situations that it's useful to call the -synchronize method in: -synchronize acts as a "barrier", in that it provides a guarantee that once it has returned, any other process that reads that default will see the new value rather than the old value.

    For applications running on iOS 9.3 and later / macOS Sierra and later, -synchronize is not needed (or recommended) even in this situation, since Key-Value Observation of defaults works between processes now, so the reading process can just watch directly for the value to change. As a result of that, applications running on those operating systems should generally never call synchronize.

    So in most likely case you do not need to set to call synchronize. It is automatically handled by KVO.

    To do this you need add observer in your classes where you are handling persistanceServiceValueChangedNotification notification. Let say you are setting a key with name "myKey"

    Add observer in your class may be viewDidLoad etc

     UserDefaults.standard.addObserver(self, forKeyPath: "myKey", options: NSKeyValueObservingOptions.new, context: nil)
    

    Handle the observer

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    
        //do your changes with for key
    }
    

    Also remove your observer in deinit

    0 讨论(0)
提交回复
热议问题