Как использовать KVO для UserDefaults в Swift?

Я переписываю части приложения и нашел этот код:

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
}

Когда CMD-щелчок по строке defaults.synchronize(), я вижу, что synchronize планируется устареть. Это написано в коде:

/*!
     -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
     */

Насколько я могу судить, использование в моем случае соответствует второму описанию: синхронизация после записи, чтобы уведомить других.

Он предлагает использовать KVO для ovserve, но как? Когда я ищу это, я нахожу кучу немного более старых Objective-C примеров. Какова наилучшая практика наблюдения UserDefaults?

Ответ 1

Как и в iOS 11 + Swift 4, рекомендуемый способ (согласно SwiftLint) использует блок-интерфейс KVO API.

Пример:

Скажем, у меня есть целочисленное значение, сохраненное в моих настройках по умолчанию, и оно называется greetingsCount.

Сначала мне нужно расширить UserDefaults:

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

Это позволяет нам определить путь ключа для наблюдения, например:

var observer: NSKeyValueObservation?

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

И никогда не забывайте очищать:

deinit {
    observer?.invalidate()
}

Ответ 2

Из блога Дэвида Смита http://dscoder.com/defaults.html https://twitter.com/catfish_man/status/674727133017587712

Если один процесс устанавливает общее значение по умолчанию, то уведомляет другой процесс прочитайте это, тогда вы можете оказаться в одной из немногих оставшихся ситуаций что полезно вызывать метод -synchronize в: -synchronize actions в качестве "барьера", в том, что он обеспечивает гарантию того, что, как только он любой другой процесс, который читает по умолчанию, увидит новый значение, а не старое значение.

Для приложений, работающих на iOS 9.3 и позже /macOS Sierra и позже, -synchronize не нужен (или рекомендуется), даже в этой ситуации, так как наблюдение значения ключа теперь по умолчанию работает между процессами, так что процесс чтения может просто смотрите непосредственно на значение, чтобы изменить. В результате этого приложения, работающие в этих операционных системах, как правило, никогда не должны синхронизировать вызов.

Так что в большинстве случаев вам не нужно устанавливать синхронизацию вызовов. Это автоматически обрабатывается KVO.

Для этого вам нужно добавить наблюдателя в ваши классы, где вы обрабатываете уведомление persistanceServiceValueChangedNotification. Допустим, вы устанавливаете ключ с именем "myKey"

Добавить наблюдателя в свой класс можно viewDidLoad и т.д.

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

Обращайтесь к наблюдателю

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

    //do your changes with for key
}

Также удалите своего наблюдателя в deinit

Ответ 3

Для тех, кто будет искать ответ в будущем, didChangeNotification будет публиковаться только в том случае, если изменения будут внесены в тот же процесс, если вы хотите получать все обновления независимо от процесса, используйте KVO.

Apple Doc

Это уведомление не публикуется при внесении изменений вне текущего процесса или при изменении вездесущих значений по умолчанию. Вы можете использовать наблюдение по значению ключа, чтобы зарегистрировать наблюдателей для определенных ключей интереса, чтобы получать уведомления обо всех обновлениях, независимо от того, были ли внесены изменения в рамках или вне текущего процесса.

Вот ссылка на демонстрационный проект Xcode, который показывает, как настроить KVO на основе блоков на UserDefaults.

Ответ 4

Версия Swift 4 с многоразовыми типами:

Файл: KeyValueObserver.swift - универсальный многократно используемый наблюдатель KVO (для случаев, когда нельзя использовать чистые наблюдаемые Swift).

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

Файл: KeyValueObserverResult.swift - тип помощника для хранения данных наблюдений KVO.

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

Файл: Observable.swift - Propocol, чтобы приостановить/возобновить и ликвидировать наблюдателя.

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

Файл: UserDefaults.swift - расширение удобства по умолчанию для пользователей.

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

Обновление настроек по умолчанию из командной строки:

# Running shell command below while sample code above is running will print 'CCC'
defaults write com.my.bundleID app-some:test_key CCC