Ist die Schlüsselwertbeobachtung (KVO) in Swift verfügbar?

174

Wenn ja, gibt es wesentliche Unterschiede, die bei Verwendung der Schlüsselwertbeobachtung in Objective-C nicht anderweitig vorhanden waren?

Codeperson
quelle
2
Ein Beispielprojekt, das die Verwendung von KVO in einer UIKit-Schnittstelle über Swift demonstriert: github.com/jameswomack/kvo-in-swift
james_womack
@JanDvorak Siehe das KVO-Programmierhandbuch , das eine schöne Einführung in das Thema darstellt.
Rob
1
Obwohl dies keine Antwort auf Ihre Frage ist, können Sie Aktionen auch mit der Funktion didset () starten.
Vincent
Beachten Sie, dass bei der Verwendung ein Swift4- Fehler auftritt .initial. Eine Lösung finden Sie hier . Ich empfehle dringend, Apple-Dokumente zu sehen . Es wurde kürzlich aktualisiert und enthält viele wichtige Hinweise. Siehe auch Robs andere Antwort
Honey

Antworten:

107

(Bearbeitet, um neue Informationen hinzuzufügen): Überlegen Sie, ob die Verwendung des Kombinations-Frameworks Ihnen dabei helfen kann, das zu erreichen, was Sie wollten, anstatt KVO zu verwenden

Ja und nein. KVO arbeitet wie immer an NSObject-Unterklassen. Es funktioniert nicht für Klassen, die NSObject nicht unterordnen. Swift verfügt (zumindest derzeit) nicht über ein eigenes natives Beobachtungssystem.

(In den Kommentaren erfahren Sie, wie Sie andere Eigenschaften als ObjC verfügbar machen, damit KVO daran arbeitet.)

Ein vollständiges Beispiel finden Sie in der Apple-Dokumentation .

Catfish_Man
quelle
74
Seit Xcode 6 Beta 5 können Sie das dynamicSchlüsselwort für jede Swift-Klasse verwenden, um die KVO-Unterstützung zu aktivieren.
Fabb
7
Hurra für @fabb! Aus Gründen der Übersichtlichkeit bezieht sich das dynamicSchlüsselwort auf die Eigenschaft, die Sie als Schlüsselwert beobachtbar machen möchten.
Jerry
5
Die Erklärung für das dynamicSchlüsselwort finden Sie im Abschnitt Verwenden von Swift mit Kakao und Objective-C der Apple Developer Library .
Imanou Petit
6
Da mir dies aus @ fabbs Kommentar nicht klar war: Verwenden Sie das dynamicSchlüsselwort für alle Eigenschaften innerhalb einer Klasse, die Sie KVO-kompatibel sein möchten (nicht das dynamicSchlüsselwort für die Klasse selbst). Das hat bei mir funktioniert!
Tim Camber
1
Nicht wirklich; Sie können ein neues didSet nicht von außen registrieren, es muss zur Kompilierungszeit Teil dieses Typs sein.
Catfish_Man
155

Sie können KVO in Swift verwenden, jedoch nur für dynamicEigenschaften der NSObjectUnterklasse. Bedenken Sie, dass Sie die barEigenschaft einer FooKlasse beobachten möchten. Geben Sie in Swift 4 barals dynamicEigenschaft in Ihrer NSObjectUnterklasse Folgendes an:

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

Sie können sich dann registrieren, um Änderungen an der barEigenschaft zu beobachten . In Swift 4 und Swift 3.2 wurde dies erheblich vereinfacht, wie unter Verwenden der Schlüsselwertbeobachtung in Swift beschrieben :

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

Beachten Sie, dass in Swift 4 jetzt Schlüsselpfade mit dem Backslash-Zeichen stark eingegeben werden (dies \.barist der Schlüsselpfad für die barEigenschaft des beobachteten Objekts). Da das Abschlussmuster für die Fertigstellung verwendet wird, müssen wir Beobachter nicht manuell entfernen (wenn der tokenGültigkeitsbereich ausfällt, wird der Beobachter für uns entfernt), und wir müssen uns auch nicht darum kümmern, die superImplementierung aufzurufen , wenn der Schlüssel dies nicht tut Spiel. Die Schließung wird nur aufgerufen, wenn dieser bestimmte Beobachter aufgerufen wird. Weitere Informationen finden Sie im WWDC 2017-Video " Was ist neu in Foundation?" .

Um dies zu beobachten, ist es in Swift 3 etwas komplizierter, aber sehr ähnlich zu dem, was man in Objective-C macht. Sie würden nämlich implementieren, observeValue(forKeyPath keyPath:, of object:, change:, context:)was (a) sicherstellt, dass wir uns mit unserem Kontext befassen (und nicht mit etwas, das unsere superInstanz zur Beobachtung registriert hat); und dann (b) entweder damit umgehen oder es nach superBedarf an die Implementierung weitergeben. Und stellen Sie sicher, dass Sie sich gegebenenfalls als Beobachter entfernen. Beispielsweise können Sie den Beobachter entfernen, wenn er freigegeben wird:

In 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])")
    }

}

Beachten Sie, dass Sie nur Eigenschaften beobachten können, die in Objective-C dargestellt werden können. Daher können Sie keine Generika, Swift- structTypen, Swift- enumTypen usw. beobachten.

Eine Diskussion der Swift 2-Implementierung finden Sie in meiner ursprünglichen Antwort unten.


Unter Verwendung des dynamicKeyword - KVO zu erreichen mit NSObjectSubklassen in dem beschriebenen Schlüsselwert Observing Abschnitt der Annahme Cocoa Designs Konventionen Kapitel der Verwendung von Swift mit Cocoa und Objective-C Anleitung:

Das Beobachten von Schlüsselwerten ist ein Mechanismus, mit dem Objekte über Änderungen an bestimmten Eigenschaften anderer Objekte benachrichtigt werden können. Sie können die Schlüsselwertbeobachtung mit einer Swift-Klasse verwenden, solange die Klasse von der NSObjectKlasse erbt . Mit diesen drei Schritten können Sie die Schlüsselwertbeobachtung in Swift implementieren.

  1. Fügen Sie den dynamicModifikator zu jeder Eigenschaft hinzu, die Sie beobachten möchten. Weitere Informationen dazu dynamicfinden Sie unter Erfordernis eines dynamischen Versands .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
    
  2. Erstellen Sie eine globale Kontextvariable.

    private var myContext = 0
  3. Fügen Sie einen Beobachter für den Schlüsselpfad hinzu, überschreiben Sie die observeValueForKeyPath:ofObject:change:context:Methode und entfernen Sie den Beobachter in 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)
        }
    }
    

[Beachten Sie, dass diese KVO-Diskussion später aus dem Handbuch „ Swift mit Kakao und Objective-C verwenden“ entfernt wurde, das für Swift 3 angepasst wurde, aber weiterhin wie oben in dieser Antwort beschrieben funktioniert.]


Es ist erwähnenswert, dass Swift über ein eigenes natives Eigenschaftsbeobachtungssystem verfügt, dies gilt jedoch für eine Klasse, die ihren eigenen Code angibt, der bei Beobachtung ihrer eigenen Eigenschaften ausgeführt wird. KVO hingegen ist so konzipiert, dass es sich registriert, um Änderungen an einer dynamischen Eigenschaft einer anderen Klasse zu beobachten.

rauben
quelle
Was ist der Zweck myContextund wie beobachten Sie mehrere Eigenschaften?
Devth
1
Laut KVO-Programmierhandbuch : "Wenn Sie ein Objekt als Beobachter registrieren, können Sie auch einen contextZeiger bereitstellen . Der contextZeiger wird dem Beobachter beim observeValueForKeyPath:ofObject:change:context:Aufrufen bereitgestellt . Der contextZeiger kann ein C-Zeiger oder eine Objektreferenz sein. Der contextZeiger kann sein wird als eindeutige Kennung verwendet, um die beobachtete Änderung zu bestimmen oder um dem Beobachter einige andere Daten zur Verfügung zu stellen. "
Rob
Sie müssen Beobachter in deinit entfernen
Jacky
3
@devth, wie ich verstehe, wird watchValueForKeyPath mehrmals aufgerufen, wenn Unterklasse oder Oberklasse auch KVO-Beobachter für dieselbe Variable registriert. Der Kontext kann verwendet werden, um eigene Benachrichtigungen in dieser Situation zu unterscheiden. Mehr dazu: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey
1
Wenn Sie optionsleer lassen, bedeutet dies nur, dass der changealte oder neue Wert nicht enthalten ist (z. B. erhalten Sie den neuen Wert möglicherweise selbst, indem Sie auf das Objekt selbst verweisen). Wenn Sie nur angeben .newund nicht .old, bedeutet dies, dass changenur der neue Wert, nicht jedoch der alte Wert enthalten ist (z. B. ist es Ihnen oft egal, was der alte Wert war, sondern nur der neue Wert). Wenn Sie observeValueForKeyPathsowohl den alten als auch den neuen Wert übergeben müssen, geben Sie an [.new, .old]. Unter dem Strich wird optionsnur angegeben, was im changeWörterbuch enthalten ist.
Rob
92

Ja und Nein:

  • Ja , Sie können dieselben alten KVO-APIs in Swift verwenden, um Objective-C-Objekte zu beobachten.
    Sie können auch dynamicEigenschaften von Swift-Objekten beobachten, von denen geerbt wird NSObject.
    Aber ... Nein, es ist nicht stark typisiert, wie man es von einem nativen Swift-Beobachtungssystem erwarten kann.
    Verwenden von Swift mit Kakao und Objective-C | Schlüsselwert beobachten

  • Nein , derzeit gibt es kein eingebautes Wertebeobachtungssystem für beliebige Swift-Objekte.

  • Ja , es gibt eingebaute Property Observers , die stark typisiert sind.
    Aber ... Nein, sie sind keine KVO, da sie nur die Beobachtung von Objekteigenschaften ermöglichen, keine verschachtelten Beobachtungen ("Schlüsselpfade") unterstützen und Sie diese explizit implementieren müssen.
    Die schnelle Programmiersprache | Immobilienbeobachter

  • Ja , Sie können eine explizite Wertebeobachtung implementieren, die stark typisiert wird, das Hinzufügen mehrerer Handler aus anderen Objekten ermöglichen und sogar Verschachtelungs- / "Schlüsselpfade" unterstützen.
    Aber ... Nein, es wird kein KVO sein, da es nur für Eigenschaften funktioniert, die Sie als beobachtbar implementieren.
    Eine Bibliothek zur Implementierung einer solchen Wertebeobachtung finden Sie hier:
    Observable-Swift - KVO für Swift - Value Observing and Events

slazyk
quelle
10

Ein Beispiel könnte hier ein wenig helfen. Wenn ich eine modelKlasseninstanz Modelmit Attributen habe nameund statediese Attribute beobachten kann mit:

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)

Änderungen an diesen Eigenschaften lösen einen Aufruf aus:

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

        println("CHANGE OBSERVED: \(change)")
}
Paul Robinson
quelle
2
Wenn ich mich nicht irre, ist der Aufrufansatz "compareValueForKeyPath" für Swift2.
Fattie
9

Ja.

KVO erfordert einen dynamischen Versand, daher müssen Sie den dynamicModifikator lediglich einer Methode, Eigenschaft, einem Index oder einem Initialisierer hinzufügen :

dynamic var foo = 0

Der dynamicModifikator stellt sicher, dass Verweise auf die Deklaration dynamisch gesendet und über abgerufen werden objc_msgSend.

Bryan Luby
quelle
7

Neben Robs Antwort. Diese Klasse muss von erben NSObject, und wir haben drei Möglichkeiten, um eine Eigenschaftsänderung auszulösen

Verwenden Sie setValue(value: AnyObject?, forKey key: String)vonNSKeyValueCoding

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

Verwenden Sie willChangeValueForKeyund didChangeValueForKeyvonNSKeyValueObserving

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

Verwenden Sie dynamic. Siehe Swift-Typkompatibilität

Sie können den dynamischen Modifikator auch verwenden, um zu verlangen, dass der Zugriff auf Mitglieder dynamisch über die Objective-C-Laufzeit verteilt wird, wenn Sie APIs wie Key-Value-Observing verwenden, die die Implementierung einer Methode dynamisch ersetzen.

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

Und Property Getter und Setter werden bei Verwendung aufgerufen. Sie können überprüfen, ob Sie mit KVO arbeiten. Dies ist ein Beispiel für eine berechnete Eigenschaft

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
quelle
5

Überblick

Es ist möglich, Combineohne NSObjectoder zu verwendenObjective-C

Verfügbarkeit: iOS 13.0+ , macOS 10.15+, tvOS 13.0+, watchOS 6.0+, Mac Catalyst 13.0+,Xcode 11.0+

Hinweis: Muss nur mit Klassen verwendet werden, nicht mit Werttypen.

Code:

Schnelle Version: 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

Ausgabe:

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

Verweisen:

user1046037
quelle
4

Derzeit unterstützt Swift keinen eingebauten Mechanismus zum Beobachten von Eigenschaftsänderungen von anderen Objekten als 'self'. Nein, KVO wird nicht unterstützt.

KVO ist jedoch ein so grundlegender Bestandteil von Objective-C und Cocoa, dass es sehr wahrscheinlich ist, dass es in Zukunft hinzugefügt wird. Die aktuelle Dokumentation scheint dies zu implizieren:

Schlüsselwertbeobachtung

Informationen folgen.

Verwenden von Swift mit Cocoa und Objective-C

ColinE
quelle
2
In diesem Handbuch, auf das Sie verweisen, wird natürlich beschrieben, wie Sie KVO in Swift ausführen.
Rob
4
Ja, jetzt implementiert ab September 2014
Max MacLeod
4

Eine wichtige Sache zu erwähnen ist, dass nach dem Aktualisieren Ihres Xcodes auf 7 Beta möglicherweise die folgende Meldung angezeigt wird: "Methode überschreibt keine Methode aus ihrer Oberklasse" . Das liegt an der Optionalität der Argumente. Stellen Sie sicher, dass Ihr Beobachtungshandler genau wie folgt aussieht:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)
Arthur Gevorkyan
quelle
2
In Xcode Beta 6 ist Folgendes erforderlich: Überschreiben von func compareValueForKeyPath (keyPath: String?, OfObject-Objekt: AnyObject?, Änderung: [String: AnyObject]?, Kontext: UnsafeMutablePointer <Void>)
hcanfly
4

Dies kann sich für wenige Menschen als hilfreich erweisen -

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

Ich hatte KVO in Swift 3 auf diese Weise verwendet. Sie können diesen Code mit wenigen Änderungen verwenden.

Nikhil Manapure
quelle
1

Ein weiteres Beispiel für alle, die auf ein Problem mit Typen wie Int? und CGFloat?. Sie legen Ihre Klasse einfach als Unterklasse von NSObject fest und deklarieren Ihre Variablen wie folgt, z.

class Theme : NSObject{

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

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

}
DrPatience
quelle