web-dev-qa-db-fra.com

Comment utiliser KVO pour UserDefaults dans Swift?

Je réécris des parties d'une application et j'ai trouvé ce code:

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
}

Lorsque CMD-cliquant sur la ligne defaults.synchronize() je vois que synchronize est prévu obsolète. Ceci est écrit dans le code:

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

Pour autant que je puisse interpréter, l'utilisation dans mon cas correspond à la deuxième description: synchronisation après écriture, afin d'en informer les autres.

Il suggère d'utiliser le KVO pour survivre, mais comment? Lorsque je recherche cela, je trouve un tas d'exemples Objective-C légèrement plus anciens. Quelle est la meilleure pratique pour observer les UserDefaults?

16
Sti

Depuis iOS 11 + Swift 4, la méthode recommandée (selon SwiftLint ) utilise l'API KVO basée sur les blocs.

Exemple:

Disons que j'ai une valeur entière stockée dans mes paramètres utilisateur par défaut et elle s'appelle greetingsCount.

Je dois d'abord étendre UserDefaults:

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

Cela nous permet de définir plus tard le chemin clé pour l'observation, comme ceci:

var observer: NSKeyValueObservation?

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

Et n'oubliez jamais de nettoyer:

deinit {
    observer?.invalidate()
}
31
Michal

Sur le blog de David Smith http://dscoder.com/defaults.htmlhttps://Twitter.com/catfish_man/status/674727133017587712

Si un processus définit une valeur par défaut partagée, puis informe un autre processus de le lire, alors vous pouvez être dans l'une des très rares situations restantes dans lesquelles il est utile d'appeler la méthode -synchronize dans: -synchronize agit comme une "barrière", en ce sens il garantit qu'une fois qu'il est retourné, tout autre processus qui lit cette valeur par défaut verra la nouvelle valeur plutôt que l'ancienne.

Pour les applications exécutées sur iOS 9.3 et versions ultérieures/macOS Sierra et versions ultérieures, -synchronize n'est pas nécessaire (ou recommandé) même dans cette situation, car l'observation par valeur-clé des valeurs par défaut fonctionne maintenant entre les processus, donc le processus de lecture peut simplement surveiller directement la valeur à changer. En conséquence, les applications exécutées sur ces systèmes d'exploitation ne devraient généralement jamais appeler synchronize.

Donc, dans le cas le plus probable, vous n'avez pas besoin de définir la synchronisation d'appel. Il est automatiquement géré par KVO.

Pour ce faire, vous devez ajouter un observateur dans vos classes où vous gérez la notification persistanceServiceValueChangedNotification. Supposons que vous définissez une clé avec le nom "myKey"

Ajouter un observateur dans votre classe peut être viewDidLoad etc

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

Gérer l'observateur

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

    //do your changes with for key
}

Supprimez également votre observateur dans deinit

8
codester

Pour tous ceux qui chercheront la réponse à l'avenir, didChangeNotification ne sera publié que si des modifications sont apportées sur le même processus, si vous souhaitez recevoir toutes les mises à jour quel que soit le processus, utilisez KVO.

doc Apple

Cette notification n'est pas publiée lorsque des modifications sont apportées en dehors du processus en cours ou lorsque des valeurs par défaut omniprésentes changent. Vous pouvez utiliser l'observation par valeur-clé pour enregistrer des observateurs pour des clés d'intérêt spécifiques afin d'être informé de toutes les mises à jour, que des modifications soient apportées au sein ou en dehors du processus en cours.

Voici un lien vers le projet de démonstration Xcode qui montre comment configurer KVO basé sur des blocs sur UserDefaults.

3
Digitech

Version Swift 4 faite avec des types réutilisables:

Fichier: KeyValueObserver.Swift - Observateur KVO réutilisable à usage général (dans les cas où les observables purs Swift observables ne peuvent pas être utilisés).

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

Fichier: KeyValueObserverResult.Swift - Type d'aide pour conserver les données d'observation 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
   }
}

Fichier: Observable.Swift - Protocole pour suspendre/reprendre et éliminer l'observateur.

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

Fichier: serDefaults.Swift - Extension pratique pour les valeurs par défaut de l'utilisateur.

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

}

tilisation:

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

Mise à jour des valeurs par défaut à partir de la ligne de commande:

# 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