How to use KVO for UserDefaults in Swift?

前端 未结 4 1592
耶瑟儿~
耶瑟儿~ 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: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: NSObject, Observable {
    
       public typealias ChangeCallback = (KeyValueObserverResult) -> 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(object: NSObject, keyPath: String,
          callback: @escaping (T) -> Void) -> Observable {
          let observer = KeyValueObserver(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(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 {
    
       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(key: String, callback: @escaping (T) -> Void) -> Observable {
          let result = KeyValueObserver.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
    

提交回复
热议问题