如何在 Swift 中将 KVO 用于 UserDefaults?

How to use KVO for UserDefaults in 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 进行监视,但是如何呢?当我搜索这个时,我发现了一堆稍旧的 Objective-C-examples。观察 UserDefaults 的最佳做法是什么?

来自 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.

所以在大多数情况下你不需要设置调用同步。它由 KVO 自动处理。

为此,您需要在处理 persistanceServiceValueChangedNotification 通知的 classes 中添加观察者。假设您正在设置一个名称为“myKey”的密钥

在你的 class 中添加观察者可能是 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

中的观察者

从 iOS 11 + Swift 4 开始,推荐的方式(根据 SwiftLint)是使用基于块的 KVO API.

示例:

假设我在用户默认值中存储了一个整数值,它被称为 greetingsCount

首先,我需要使用 dynamic var 扩展 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()
}

对于将来寻找答案的任何人,didChangeNotification 仅当对同一进程进行更改时才会发布,如果您希望接收所有更新而不管进程如何使用 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.

这里有一个 link to demo Xcode project,展示了如何在 UserDefaults 上设置基于块的 KVO。

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 - 协议 suspend/resume 并处置观察者。

public protocol Observable {
   var isSuspended: Bool { get set }
   func dispose()
}

extension Array where Element == Observable {

   public func suspend() {
      forEach {
         var observer = [=12=]
         observer.isSuspended = true
      }
   }

   public func resume() {
      forEach {
         var observer = [=12=]
         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([=13=])
      }
      return result
   }

   public func observeString(key: String, callback: @escaping (String) -> Void) -> Observable {
      return observe(key: key, callback: callback)
   }

}

用法:

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([=14=]) // 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

自 iOS13 起,现在有一种更酷的方法可以使用 Combine:

import Foundation
import Combine

extension UserDefaults {
    /// Observe UserDefaults for changes at the supplied KeyPath.
    ///
    /// Note: first, extend UserDefaults with an `@objc dynamic` variable
    /// to create a KeyPath.
    ///
    /// - Parameters:
    ///   - keyPath: the KeyPath to observe for changes.
    ///   - handler: closure to run when/if the value changes.
    public func observe<T>(
        _ keyPath: KeyPath<UserDefaults, T>,
        handler: @escaping (T) -> Void)
    {
        let subscriber = Subscribers.Sink<T, Never> { _ in }
            receiveValue: { newValue in
                handler(newValue)
            }
        
        self.publisher(for: keyPath, options: [.initial, .new])
            .subscribe(subscriber)
    }
}