web-dev-qa-db-fra.com

Vérifier si une valeur est modifiée à l'aide de KVO dans Swift 3

J'aimerais savoir quand un ensemble de propriétés d'un objet Swift change. Auparavant, j'avais implémenté ceci dans Objective-C, mais j'ai du mal à le convertir en Swift.

Mon code Objective-C précédent est:

- (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
    if (![change[@"new"] isEqual:change[@"old"]])
        [self edit];
}

Mon premier passage à une solution Swift a été:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if change?[.newKey] != change?[.oldKey] {    // Compiler: "Binary operator '!=' cannot be applied to two 'Any?' operands"
        edit()
    }
}

Cependant, le compilateur se plaint: "L'opérateur binaire '! =' Ne peut pas être appliqué à deux" Any? " opérandes "

Ma deuxième tentative:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let newValue = change?[.newKey] as? NSObject {
        if let oldValue = change?[.oldKey] as? NSObject {
            if !newValue.isEqual(oldValue) {
                edit()
            }
        }
    }
}

Mais, en y réfléchissant, je ne pense pas que cela fonctionnera pour les primitives d’objets Swift tels que Int qui (je suppose) n’héritent pas de NSObject et, contrairement à la version Objective-C, ne seront pas encadrées dans NSNumber lorsqu’elles seront placées dans le dictionnaire de changement.

La question est donc de savoir comment puis-je effectuer la tâche apparemment facile de déterminer si une valeur est réellement modifiée à l'aide du KVO dans Swift3?

Aussi, question bonus, comment utiliser la variable 'of object'? Cela ne me permet pas de changer le nom et, bien sûr, n'aime pas les variables contenant des espaces.

18
aepryus

Vous trouverez ci-dessous ma réponse initiale à Swift 3, mais Swift 4 simplifie le processus en éliminant le recours à un casting. Par exemple, si vous observez la propriété Int appelée bar de l'objet foo:

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

class ViewController: UIViewController {

    let foo = Foo()
    var token: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()

        token = foo.observe(\.bar, options: [.new, .old]) { [weak self] object, change in
            if change.oldValue != change.newValue {
                self?.edit()
            }
        }
    }

    func edit() { ... }
}

Notez, cette approche basée sur la fermeture:

  • Vous évite d'avoir à implémenter une méthode observeValue distincte;

  • Élimine la nécessité de spécifier une context et de vérifier ce contexte; et

  • Les change.newValue et change.oldValue sont correctement saisis, éliminant le besoin de lancer manuellement. Si la propriété était facultative, vous devrez peut-être les dérouler en toute sécurité, mais aucun casting n'est nécessaire.

La seule chose à laquelle vous devez faire attention est de vous assurer que votre fermeture n'introduit pas un cycle de référence fort (d'où l'utilisation du modèle [weak self]).


Ma réponse originale à Swift 3 est ci-dessous.


Tu as dit:

Mais en y réfléchissant, je ne pense pas que cela fonctionnera pour les primitives d’objets Swift tels que Int qui (je suppose) n’héritent pas de NSObject et contrairement à la version Objective-C ne seront pas encadrés dans NSNumber le dictionnaire de changement.

En fait, si vous regardez ces valeurs, si la propriété observée est une Int, elle apparaît dans le dictionnaire sous la forme d'une NSNumber.

Vous pouvez donc rester dans le monde NSObject:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let newValue = change?[.newKey] as? NSObject,
        let oldValue = change?[.oldKey] as? NSObject,
        !newValue.isEqual(oldValue) {
            edit()
    }
}

Ou utilisez-les comme NSNumber:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let newValue = change?[.newKey] as? NSNumber,
        let oldValue = change?[.oldKey] as? NSNumber,
        newValue.intValue != oldValue.intValue {
            edit()
    }
}

Ou bien, s'il s'agissait d'une valeur Int d'une propriété dynamic d'une classe Swift, j'irais de l'avant et je les jetterais sous la forme Int:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let newValue = change?[.newKey] as? Int, let oldValue = change?[.oldKey] as? Int, newValue != oldValue {
        edit()
    }
}

Tu as demandé:

Aussi, question bonus, comment utiliser la variable of object? Cela ne me permet pas de changer le nom et, bien sûr, n'aime pas les variables contenant des espaces.

La of est l'étiquette externe de ce paramètre (utilisé lorsque vous appelez cette méthode; dans ce cas, le système d'exploitation l'appelle pour nous, nous n'utilisons donc pas cette étiquette externe si elle n'est pas dans la signature de la méthode). La object est l'étiquette interne (utilisée dans la méthode elle-même). Swift a la capacité pour les étiquettes externes et internes pour les paramètres depuis un moment, mais cela n’a vraiment été adopté dans l’API à partir de Swift 3.

Lorsque vous utilisez ce paramètre change, vous l’utilisez si vous observez les propriétés de plusieurs objets et si ces objets nécessitent une gestion différente sur le KVO, par exemple:

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

Et alors:

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
    }

    if (object as? Foo) == foo {
        // handle `foo` related notifications here
    }
    if (object as? Baz) == baz {
        // handle `baz` related notifications here
    }
}

En passant, je recommanderais généralement d’utiliser la variable context, par exemple, un private var:

private var observerContext = 0

Et puis ajoutez l'observateur utilisant ce contexte:

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

Et ensuite, assurez-vous que ma observeValue est bien sa context, et non celle établie par sa superclasse:

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
    }

    if let newValue = change?[.newKey] as? Int, let oldValue = change?[.oldKey] as? Int, newValue != oldValue {
        edit()
    }
}
31
Rob

Ma "deuxième tentative" d'origine contient un bogue qui ne parvient pas à détecter lorsqu'une valeur est modifiée ou annulée. La première tentative peut en fait être corrigée une fois que l'on se rend compte que toutes les valeurs de 'change' sont NSObject:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    let oldValue: NSObject? = change?[.oldKey] as? NSObject
    let newValue: NSObject? = change?[.newKey] as? NSObject
    if newValue != oldValue {
        edit()
    }
}

ou une version plus concise si vous préférez:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if change?[.newKey] as? NSObject != change?[.oldKey] as? NSObject {
        edit()
    }
}
0
aepryus