如何在Swift中使用KVO for UserDefaults?

时间:2017-05-14 11:13:29

标签: ios swift key-value-observing foundation

我正在重写部分应用,并找到了这段代码:

defaults.synchronize()

当CMD点击第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 */ 。这写在代码中:

{{1}}

据我所知,我的用法符合第二种描述:在写完后同步,以便通知其他人。

它建议使用KVO来养卵,但是如何?当我搜索这个时,我发现了一堆稍微旧的Objective-C-examples。观察UserDefaults的最佳做法是什么?

5 个答案:

答案 0 :(得分:22)

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

答案 1 :(得分:6)

来自David Smith的博客 http://dscoder.com/defaults.html https://twitter.com/catfish_man/status/674727133017587712

  

如果一个进程设置了共享默认值,则通知另一个进程   阅读它,然后你可能处于剩下的极少数情况之一   在-synchronize中调用-synchronize方法很有用   作为一个“障碍”,它提供了一旦它拥有的保证   返回,任何其他读取默认值的进程都会看到新的   价值而不是旧价值。

     

适用于在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

中的观察者

答案 2 :(得分:1)

对于以后将要寻找答案的任何人,didChangeNotification仅在对同一过程进行了更改的情况下发布,并且无论该过程是否使用KVO,您都希望收到所有更新。 / p>

Apple doc

  

在当前流程之外进行更改或无处不在的默认值更改时,不会发布此通知。您可以使用键值观察为特定的特定键注册观察者,以便将所有更新通知给您,而不管是在当前过程之内还是之外进行更改。

这里是link to demo Xcode project,显示了如何在UserDefaults上设置基于块的KVO。

答案 3 :(得分:0)

使用可重用类型的Swift 4版本:

文件: KeyValueObserver.swift -通用可重复使用的KVO观察器(用于无法使用纯Swift观察值的情况)。

var fieldVal = getObject()
    .map(obj -> obj.getNullableField())
    .orElseThrow(() -> new IllegalStateException("Object not found!"));

  return Optional.ofNullable(fieldVal)
     .orElseThrow(() -> new IllegalStateException("Field is not present"));

文件: KeyValueObserverResult.swift –用于保留KVO观测数据的帮助程序类型。

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

文件: Observable.swift -暂停/恢复和处置观察者的协议。

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

文件: UserDefaults.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
      }
   }
}

用法

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

答案 4 :(得分:0)

从 iOS 13 开始,现在有一种更酷的方法来做到这一点,使用结合:

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