【问题标题】:How to use KVO for UserDefaults in Swift?如何在 Swift 中将 KVO 用于 UserDefaults?
【发布时间】:2017-10-13 06:40:30
【问题描述】:

我正在重写应用程序的某些部分,并找到了以下代码:

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 的最佳做法是什么?

【问题讨论】:

标签: ios swift key-value-observing foundation


【解决方案1】:

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

示例:

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

首先我需要扩展UserDefaultsdynamic var,它与您要观察的用户默认键具有同名

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

【讨论】:

  • 谢谢。你的observe 语句中greetingsCount 之前的\. 是什么意思?
  • @CliftonLabrum 这是一个名为 Key Path 的新 Swift 4 构造。更多关于 here.
  • 这在 macOS 中似乎对我不起作用:/。我使用的是 App Group 用户默认组,而不是 StandardUserDefaults。观察者闭包在初始化时被调用一次(给出 nil 值),然后在发生更改时不再调用。此处 cmets 中的类似问题:stackoverflow.com/questions/44424971/…
  • 这个答案很好。我想指出,方法名称必须与 userdefaults 对象名称相匹配。如果您改为使用var myGreetingsCount,您将永远不会收到观察者回调。
  • 你不必清理观察者。见:stackoverflow.com/a/46591915
【解决方案2】:

来自大卫·史密斯的博客 http://dscoder.com/defaults.html https://twitter.com/catfish_man/status/674727133017587712

如果一个进程设置了共享默认值,则通知另一个进程 阅读它,那么您可能处于为数不多的几种情况之一 在以下位置调用 -synchronize 方法很有用: -synchronize 行为 作为一个“障碍”,因为它提供了一个保证,一旦它已经 返回,读取该默认值的任何其他进程将看到新的 值而不是旧值。

适用于在 iOS 9.3 上运行的应用程序 及更高版本/macOS Sierra 及更高版本,不需要 -synchronize(或 推荐)即使在这种情况下,因为 Key-Value Observation 默认值现在在进程之间工作,因此读取过程可以 直接观察值的变化。结果, 在这些操作系统上运行的应用程序通常不应该 调用同步。

所以在大多数情况下,您不需要设置调用同步。它由 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

    当在当前流程之外进行更改或普遍存在的默认值发生更改时,不会发布此通知。您可以使用键值观察为感兴趣的特定键注册观察者,以便收到所有更新的通知,无论是在当前流程内部还是外部进行更改。

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

    【讨论】:

      【解决方案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 - 暂停/恢复和处置观察者的协议。

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

      用法

      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
      

      【讨论】:

      • 这种设计失去了上下文参数的威力——它允许子类添加更多的键,它们与父类使用的上下文相同,从而导致调用相同的动作。
      【解决方案5】:

      从 iOS 13 开始,现在有一种更酷的方法可以做到这一点,即使用 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)
          }
      }
      

      【讨论】:

      • 你至少应该添加一个例子。
      猜你喜欢
      • 2015-09-21
      • 2017-11-15
      • 1970-01-01
      • 2021-06-08
      • 1970-01-01
      • 1970-01-01
      • 2018-03-17
      • 1970-01-01
      相关资源
      最近更新 更多