How can I use KVO for UserDefaults in Swift?

I am rewriting parts of the application and found this code:

fileprivate let defaults = UserDefaults.standard

func storeValue(_ value: AnyObject, forKey key:String) {
    defaults.set(value, forKey: key)
    defaults.synchronize()

    NotificationCenter.default.post(name: Notification.Name(rawValue: "persistanceServiceValueChangedNotification"), object: key)
}
func getValueForKey(_ key:String, defaultValue:AnyObject? = nil) -> AnyObject? {
    return defaults.object(forKey: key) as AnyObject? ?? defaultValue
}

      

When CMD-clicking on the line defaults.synchronize()

I see that it synchronize

is planned to be deprecated. This is written in code:

/*!
     -synchronize is deprecated and will be marked with the NS_DEPRECATED macro in a future release.

     -synchronize blocks the calling thread until all in-progress set operations have completed. This is no longer necessary. Replacements for previous uses of -synchronize depend on what the intent of calling synchronize was. If you synchronized...
     - ...before reading in order to fetch updated values: remove the synchronize call
     - ...after writing in order to notify another program to read: the other program can use KVO to observe the default without needing to notify
     - ...before exiting in a non-app (command line tool, agent, or daemon) process: call CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication)
     - ...for any other reason: remove the synchronize call
     */

      

As far as I can tell, the usage in my case matches the second description: sync after recording to notify others.

He suggests using KVO for ovserve, but how? When I search for this, I find a bunch of slightly older Objective-C examples. What's the best practice for observing UserDefaults?

+15


source to share


4 answers


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

Example:

Let's say I have an integer value stored in my default settings and it's called greetingsCount

.

First I need to expand UserDefaults

:

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

      



This allows us to define the path of the key to observe, for example:

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

      

+28


source


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

If one process sets a common default, then notifies another to read this, then you may find yourself in one of the few remaining situations that it is useful to call the -synchronize method in: -synchronize actions as a "barrier" in that it provides a guarantee that as soon as any other process that reads by default will see the new value and not the old value.

For apps running on iOS 9.3 and later / macOS Sierra and later, -synchronize is unnecessary (or recommended), even in this situation, as key value observation now works cross-process by default, so the reading process can just look directly at value to change. As a result, applications running on these operating systems typically never need to synchronize a call.

So in most cases you don't need to set up call sync. This is automatically handled by KVO.

To do this, you need to add an observer to your classes where you handle the notification persistanceServiceValueChangedNotification

. Let's say you are installing a key named "myKey"

You can add an observer to your class viewDidLoad

, etc.



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

      

Contact an 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

+8


source


For those looking for an answer in the future, didChangeNotification

will only post if changes are made in the same process, if you want to receive all updates regardless of the process, use KVO.

Apple doc

This notice is not posted when changes are made outside the current process, or when the ubiquitous defaults are changed. You can use key-value observation to register observers for specific keys of interest to be notified of all updates, whether changes were made inside or outside the current process.

Here is a link to an Xcode demo project that shows how to set up block-based KVO on UserDefaults.

+2


source


Swift 4 version with reusable types:

File: KeyValueObserver.swift - A generic reusable KVO observer (for cases where pure Swift observables cannot 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 for storing 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 kill the 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 - default convenience extension for users.

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 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 default settings from the 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


source







All Articles