L'observation de valeurs-clés (KVO) est-elle disponible dans Swift?

174

Si tel est le cas, existe-t-il des différences clés qui n'étaient pas présentes lors de l'utilisation de l'observation clé-valeur dans Objective-C?

codeperson
la source
2
Un exemple de projet qui démontre l'utilisation de KVO dans une interface UIKit via Swift: github.com/jameswomack/kvo-in-swift
james_womack
@JanDvorak Consultez le Guide de programmation du KVO , qui est une belle introduction au sujet.
Rob
1
Bien que ce ne soit pas une réponse à votre question, vous pouvez également lancer des actions à l'aide de la fonction didset ().
Vincent
Notez qu'il y a un bogue Swift4 lorsque vous utilisez .initial. Pour une solution, voir ici . Je recommande vivement de consulter les documents Apple . Il a été mis à jour récemment et couvre de nombreuses notes importantes. Voir aussi l' autre réponse de
Honey

Réponses:

107

(Modifié pour ajouter de nouvelles informations): considérez si l'utilisation du framework Combine peut vous aider à accomplir ce que vous vouliez, plutôt que d'utiliser KVO

Oui et non. KVO fonctionne sur les sous-classes NSObject comme il l'a toujours fait. Cela ne fonctionne pas pour les classes qui ne sous-classent pas NSObject. Swift n'a pas (du moins actuellement) son propre système d'observation natif.

(Voir les commentaires pour savoir comment exposer d'autres propriétés en tant qu'ObjC afin que KVO fonctionne dessus)

Consultez la documentation Apple pour un exemple complet.

Catfish_Man
la source
74
Depuis Xcode 6 beta 5, vous pouvez utiliser le dynamicmot - clé sur n'importe quelle classe Swift pour activer la prise en charge de KVO.
fabb
7
Hourra pour @fabb! Pour plus de clarté, le dynamicmot clé va sur la propriété que vous souhaitez rendre observable clé-valeur.
Jerry
5
L'explication du dynamicmot - clé peut être trouvée dans la section Utilisation de Swift avec Cocoa et Objective-C de la bibliothèque des développeurs Apple .
Imanou Petit
6
Comme ce n'était pas clair pour moi à partir du commentaire de @ fabb: utilisez le dynamicmot - clé pour toutes les propriétés à l' intérieur d'une classe que vous aimeriez être compatible KVO (pas le dynamicmot - clé sur la classe elle-même). Cela a fonctionné pour moi!
Tim Camber
1
Pas vraiment; vous ne pouvez pas enregistrer un nouveau didSet de "l'extérieur", il doit faire partie de ce type au moment de la compilation.
Catfish_Man
155

Vous pouvez utiliser KVO dans Swift, mais uniquement pour les dynamicpropriétés de la NSObjectsous - classe. Considérez que vous vouliez observer la barpropriété d'une Fooclasse. Dans Swift 4, spécifiez barcomme dynamicpropriété dans votre NSObjectsous-classe:

class Foo: NSObject {
    @objc dynamic var bar = 0
}

Vous pouvez ensuite vous inscrire pour observer les modifications apportées à la barpropriété. Dans Swift 4 et Swift 3.2, cela a été considérablement simplifié, comme indiqué dans Utilisation de l'observation des valeurs-clés dans Swift :

class MyObject {
    private var token: NSKeyValueObservation

    var objectToObserve = Foo()

    init() {
        token = objectToObserve.observe(\.bar) { [weak self] object, change in  // the `[weak self]` is to avoid strong reference cycle; obviously, if you don't reference `self` in the closure, then `[weak self]` is not needed
            print("bar property is now \(object.bar)")
        }
    }
}

Notez que dans Swift 4, nous avons maintenant un typage fort des chemins de clé à l'aide du caractère barre oblique inverse ( \.barc'est le chemin de clé pour la barpropriété de l'objet observé). De plus, comme il utilise le modèle de fermeture de complétion, nous n'avons pas à supprimer manuellement les observateurs (lorsque l'observateur tokentombe hors de portée, l'observateur est supprimé pour nous) ni à nous soucier d'appeler l' superimplémentation si la clé ne le fait pas rencontre. La fermeture est appelée uniquement lorsque cet observateur particulier est appelé. Pour plus d'informations, consultez la vidéo WWDC 2017, Nouveautés de Foundation .

Dans Swift 3, pour observer cela, c'est un peu plus compliqué, mais très similaire à ce que l'on fait en Objective-C. À savoir, vous implémenteriez observeValue(forKeyPath keyPath:, of object:, change:, context:)ce qui (a) garantit que nous traitons avec notre contexte (et non quelque chose que notre superinstance avait enregistré pour observer); puis (b) soit le manipuler, soit le transmettre à l' superimplémentation, si nécessaire. Et assurez-vous de vous retirer en tant qu'observateur le cas échéant. Par exemple, vous pouvez supprimer l'observateur lorsqu'il est désalloué:

Dans Swift 3:

class MyObject: NSObject {
    private var observerContext = 0

    var objectToObserve = Foo()

    override init() {
        super.init()

        objectToObserve.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)
    }

    deinit {
        objectToObserve.removeObserver(self, forKeyPath: #keyPath(Foo.bar), context: &observerContext)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &observerContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }

        // do something upon notification of the observed object

        print("\(keyPath): \(change?[.newKey])")
    }

}

Notez que vous ne pouvez observer que les propriétés qui peuvent être représentées en Objective-C. Ainsi, vous ne pouvez pas observer les génériques, les structtypes Swift, les enumtypes Swift , etc.

Pour une discussion sur l'implémentation de Swift 2, voir ma réponse originale ci-dessous.


L'utilisation du dynamicmot - clé pour atteindre KVO avec des NSObjectsous-classes est décrite dans la section Observation des valeurs-clés du chapitre Adopting Cocoa Design Conventions du guide Utilisation de Swift avec Cocoa et Objective-C :

L'observation des valeurs-clés est un mécanisme qui permet aux objets d'être notifiés des modifications apportées aux propriétés spécifiées d'autres objets. Vous pouvez utiliser l'observation clé-valeur avec une classe Swift, à condition que la classe hérite de la NSObjectclasse. Vous pouvez utiliser ces trois étapes pour implémenter l'observation des valeurs-clés dans Swift.

  1. Ajoutez le dynamicmodificateur à toute propriété que vous souhaitez observer. Pour plus d'informations sur dynamic, voir Exiger une répartition dynamique .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
    
  2. Créez une variable de contexte globale.

    private var myContext = 0
  3. Ajoutez un observateur pour le chemin d'accès aux clés, remplacez la observeValueForKeyPath:ofObject:change:context:méthode et supprimez l'observateur dans deinit.

    class MyObserver: NSObject {
        var objectToObserve = MyObjectToObserve()
        override init() {
            super.init()
            objectToObserve.addObserver(self, forKeyPath: "myDate", options: .New, context: &myContext)
        }
    
        override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
            if context == &myContext {
                if let newValue = change?[NSKeyValueChangeNewKey] {
                    print("Date changed: \(newValue)")
                }
            } else {
                super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
            }
        }
    
        deinit {
            objectToObserve.removeObserver(self, forKeyPath: "myDate", context: &myContext)
        }
    }
    

[Notez que cette discussion KVO a par la suite été supprimée du guide Utilisation de Swift avec Cocoa et Objective-C , qui a été adapté pour Swift 3, mais il fonctionne toujours comme indiqué en haut de cette réponse.]


Il convient de noter que Swift a son propre système d' observateur de propriétés natif , mais c'est pour une classe spécifiant son propre code qui sera exécuté après l'observation de ses propres propriétés. KVO, d'autre part, est conçu pour s'enregistrer pour observer les changements de certaines propriétés dynamiques d'une autre classe.

Rob
la source
À quoi sert myContextet comment observez-vous plusieurs propriétés?
devth
1
Selon le Guide de programmation KVO : "Lorsque vous enregistrez un objet en tant qu'observateur, vous pouvez également fournir un contextpointeur. Le contextpointeur est fourni à l'observateur lorsqu'il observeValueForKeyPath:ofObject:change:context:est appelé. Le contextpointeur peut être un pointeur C ou une référence d'objet. Le contextpointeur peut être utilisé comme identifiant unique pour déterminer le changement observé ou pour fournir d'autres données à l'observateur. "
Rob
vous devez supprimer l'observateur à deinit
Jacky
3
@devth, si je comprends bien, si la sous-classe ou la superclasse enregistre également l'observateur KVO pour la même variable, observeValueForKeyPath sera appelé plusieurs fois. Le contexte peut être utilisé pour distinguer ses propres notifications dans cette situation. Plus à ce sujet: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey
1
Si vous laissez optionsvide, cela signifie simplement que le changen'inclura pas l'ancienne ou la nouvelle valeur (par exemple, vous pourriez simplement obtenir la nouvelle valeur vous-même en référençant l'objet lui-même). Si vous spécifiez simplement .newet non .old, cela signifie que changecela n'inclura que la nouvelle valeur, mais pas l'ancienne valeur (par exemple, vous ne vous souciez souvent pas de ce qu'était l'ancienne valeur, mais uniquement de la nouvelle valeur). Si vous devez vous observeValueForKeyPathtransmettre à la fois l'ancienne et la nouvelle valeur, spécifiez [.new, .old]. En bout de ligne, optionsspécifie simplement ce qui est inclus dans le changedictionnaire.
Rob
92

Oui et non:

  • Oui , vous pouvez utiliser les mêmes anciennes API KVO dans Swift pour observer les objets Objective-C.
    Vous pouvez également observer les dynamicpropriétés des objets Swift héritant de NSObject.
    Mais ... Non, ce n'est pas fortement typé comme vous pouvez vous attendre à ce que le système d'observation natif Swift le soit.
    Utilisation de Swift avec Cocoa et Objective-C | Observation des valeurs clés

  • Non , il n'y a actuellement pas de système d'observation des valeurs intégré pour les objets Swift arbitraires.

  • Oui , il existe des observateurs de propriétés intégrés , qui sont fortement typés.
    Mais ... Non, ils ne sont pas KVO, puisqu'ils permettent seulement d'observer les propriétés propres des objets, ne supportent pas les observations imbriquées ("chemins clés"), et vous devez les implémenter explicitement.
    Le langage de programmation Swift | Observateurs immobiliers

  • Oui , vous pouvez implémenter l'observation de valeur explicite, qui sera fortement typée, et autoriser l'ajout de plusieurs gestionnaires à partir d'autres objets, et même prendre en charge l'imbrication / les "chemins de clé".
    Mais ... Non, ce ne sera pas KVO car il ne fonctionnera que pour les propriétés que vous implémentez comme observables.
    Vous pouvez trouver une bibliothèque pour implémenter une telle observation de valeur ici:
    Observable-Swift - KVO pour Swift - Observation de valeur et événements

Slazyk
la source
10

Un exemple pourrait aider un peu ici. Si j'ai une instance modelde classe Modelavec des attributs nameet que stateje peux observer ces attributs avec:

let options = NSKeyValueObservingOptions([.New, .Old, .Initial, .Prior])

model.addObserver(self, forKeyPath: "name", options: options, context: nil)
model.addObserver(self, forKeyPath: "state", options: options, context: nil)

Les modifications apportées à ces propriétés déclencheront un appel à:

override func observeValueForKeyPath(keyPath: String!,
    ofObject object: AnyObject!,
    change: NSDictionary!,
    context: CMutableVoidPointer) {

        println("CHANGE OBSERVED: \(change)")
}
Paul Robinson
la source
2
Si je ne me trompe pas, l'approche d'appel observeValueForKeyPath est pour Swift2.
Fattie
9

Oui.

KVO nécessite une répartition dynamique, il vous suffit donc d'ajouter le dynamicmodificateur à une méthode, une propriété, un indice ou un initialiseur:

dynamic var foo = 0

Le dynamicmodificateur garantit que les références à la déclaration seront distribuées dynamiquement et accessibles via objc_msgSend.

Bryan Luby
la source
7

En plus de la réponse de Rob. Cette classe doit hériter de NSObject, et nous avons 3 façons de déclencher un changement de propriété

Utiliser à setValue(value: AnyObject?, forKey key: String)partir deNSKeyValueCoding

class MyObjectToObserve: NSObject {
    var myDate = NSDate()
    func updateDate() {
        setValue(NSDate(), forKey: "myDate")
    }
}

Utiliser willChangeValueForKeyet didChangeValueForKeydeNSKeyValueObserving

class MyObjectToObserve: NSObject {
    var myDate = NSDate()
    func updateDate() {
        willChangeValueForKey("myDate")
        myDate = NSDate()
        didChangeValueForKey("myDate")
    }
}

Utilisez dynamic. Voir la compatibilité des types Swift

Vous pouvez également utiliser le modificateur dynamique pour exiger que l'accès aux membres soit distribué dynamiquement via le runtime Objective-C si vous utilisez des API telles que l'observation clé-valeur qui remplacent dynamiquement l'implémentation d'une méthode.

class MyObjectToObserve: NSObject {
    dynamic var myDate = NSDate()
    func updateDate() {
        myDate = NSDate()
    }
}

Et la propriété getter et setter est appelée lorsqu'elle est utilisée. Vous pouvez vérifier lorsque vous travaillez avec KVO. Ceci est un exemple de propriété calculée

class MyObjectToObserve: NSObject {
    var backing: NSDate = NSDate()
    dynamic var myDate: NSDate {
        set {
            print("setter is called")
            backing = newValue
        }
        get {
            print("getter is called")
            return backing
        }
    }
}
onmyway133
la source
5

Aperçu

Il est possible d'utiliser Combinesans utiliser NSObjectouObjective-C

Disponibilité: iOS 13.0+ , macOS 10.15+, tvOS 13.0+, watchOS 6.0+, Mac Catalyst 13.0+,Xcode 11.0+

Remarque: doit être utilisé uniquement avec des classes et non avec des types valeur.

Code:

Version Swift: 5.1.2

import Combine //Combine Framework

//Needs to be a class doesn't work with struct and other value types
class Car {

    @Published var price : Int = 10
}

let car = Car()

//Option 1: Automatically Subscribes to the publisher

let cancellable1 = car.$price.sink {
    print("Option 1: value changed to \($0)")
}

//Option 2: Manually Subscribe to the publisher
//Using this option multiple subscribers can subscribe to the same publisher

let publisher = car.$price

let subscriber2 : Subscribers.Sink<Int, Never>

subscriber2 = Subscribers.Sink(receiveCompletion: { print("completion \($0)")}) {
    print("Option 2: value changed to \($0)")
}

publisher.subscribe(subscriber2)

//Assign a new value

car.price = 20

Production:

Option 1: value changed to 10
Option 2: value changed to 10
Option 1: value changed to 20
Option 2: value changed to 20

Référer:

utilisateur1046037
la source
4

Actuellement, Swift ne prend en charge aucun mécanisme intégré pour observer les changements de propriété d'objets autres que «self», donc non, il ne prend pas en charge KVO.

Cependant, KVO est une partie tellement fondamentale d'Objective-C et de Cocoa qu'il semble fort probable qu'il sera ajouté à l'avenir. La documentation actuelle semble impliquer ceci:

Observation des valeurs-clés

Information à venir.

Utilisation de Swift avec Cocoa et Objective-C

Coline
la source
2
De toute évidence, ce guide auquel vous faites référence décrit maintenant comment faire KVO dans Swift.
Rob
4
Oui, maintenant mis en œuvre à partir de septembre 2014
Max MacLeod
4

Une chose importante à mentionner est qu'après la mise à jour de votre Xcode vers la version 7 beta, vous pourriez recevoir le message suivant: "La méthode ne remplace aucune méthode de sa superclasse" . C'est à cause du caractère facultatif des arguments. Assurez-vous que votre gestionnaire d'observation ressemble exactement à ce qui suit:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)
Arthur Gevorkyan
la source
2
Dans Xcode beta 6, il nécessite: override func observeValueForKeyPath (keyPath: String ?, ofObject object: AnyObject ?, change: [String: AnyObject] ?, context: UnsafeMutablePointer <Void>)
hcanfly
4

Cela peut être utile à peu de gens -

// MARK: - KVO

var observedPaths: [String] = []

func observeKVO(keyPath: String) {
    observedPaths.append(keyPath)
    addObserver(self, forKeyPath: keyPath, options: [.old, .new], context: nil)
}

func unObserveKVO(keyPath: String) {
    if let index = observedPaths.index(of: keyPath) {
        observedPaths.remove(at: index)
    }
    removeObserver(self, forKeyPath: keyPath)
}

func unObserveAllKVO() {
    for keyPath in observedPaths {
        removeObserver(self, forKeyPath: keyPath)
    }
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let keyPath = keyPath {
        switch keyPath {
        case #keyPath(camera.iso):
            slider.value = camera.iso
        default:
            break
        }
    }
}

J'avais utilisé KVO de cette manière dans Swift 3. Vous pouvez utiliser ce code avec quelques modifications.

Nikhil Manapure
la source
1

Un autre exemple pour quiconque rencontre un problème avec des types tels que Int? et CGFloat ?. Vous définissez simplement votre classe comme une sous-classe de NSObject et déclarez vos variables comme suit, par exemple:

class Theme : NSObject{

   dynamic var min_images : Int = 0
   dynamic var moreTextSize : CGFloat = 0.0

   func myMethod(){
       self.setValue(value, forKey: "\(min_images)")
   }

}
DrPatience
la source