web-dev-qa-db-ja.com

SwiftでUserDefaultsにKVOを使用する方法

アプリの一部を書き直しているところ、次のコードが見つかりました。

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

私が解釈できる限り、私の場合の使用法は2番目の説明に適合します。他の人に通知するために、after書き込み後に同期します。

OvserveにKVOを使用することをお勧めしますが、どうやって?これを検索すると、少し古いObjective-C-examplesがたくさん見つかります。 UserDefaultsを監視するためのベストプラクティスは何ですか?

16
Sti

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

デビッド・スミスのブログから http://dscoder.com/defaults.htmlhttps://Twitter.com/catfish_man/status/674727133017587712

あるプロセスが共有デフォルトを設定し、別のプロセスにそれを読み取るよう通知する場合、-synchronizeメソッドを呼び出すと便利な非常に少数の残りの状況の1つになる可能性があります。-synchronizeは「バリア」として機能しますこれは、いったん戻った後、そのデフォルトを読み取る他のプロセスが古い値ではなく新しい値を参照することを保証します。

iOS 9.3以降/ macOS Sierra以降で実行されているアプリケーションでは、この状況でも-synchronizeは必要ありません(または推奨されていません)。これは、デフォルトのキー値監視がプロセス間で機能するためです。読み取りプロセスは、値が変化するのを直接監視するだけです。その結果、これらのオペレーティングシステムで実行されているアプリケーションは、通常、同期を呼び出すことはできません。

したがって、ほとんどの場合、同期を呼び出すように設定する必要はありません。 KVOによって自動的に処理されます。

これを行うには、persistanceServiceValueChangedNotification通知を処理するクラスにオブザーバーを追加する必要があります。 「myKey」という名前のキーを設定するとします

あなたのクラスにオブザーバーを追加するかもしれませんviewDidLoad etc

 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のオブザーバーも削除します

8
codester

将来の回答を探しているすべての人にとって、didChangeNotificationは、同じプロセスで変更が行われた場合にのみ投稿されます。プロセスに関係なくすべての更新を受け取りたい場合は、KVOを使用します。

Appleドキュメント

現在のプロセス外で変更が加えられた場合、またはユビキタスデフォルトが変更された場合、この通知は投稿されません。キー値監視を使用すると、現在のプロセス内で行われたか、外部で変更されたかに関係なく、すべての更新について通知を受けるために、特定のキーのオブザーバーを登録できます。

以下は デモXcodeプロジェクトへのリンク で、UserDefaultsでブロックベースのKVOをセットアップする方法を示しています。

3
Digitech

再利用可能なタイプで作成されたSwift 4バージョン:

ファイル:KeyValueObserver.Swift-汎用の再利用可能なKVOオブザーバー(pure Swift observablesを使用できない場合)。

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

ファイル:serDefaults.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
1
Vlad