Czy obserwacja klucz-wartość (KVO) jest dostępna w języku Swift?

174

Jeśli tak, czy są jakieś kluczowe różnice, które nie byłyby obecne podczas korzystania z obserwacji klucz-wartość w Objective-C?

kodeperson
źródło
2
Przykładowy projekt, który demonstruje użycie KVO w interfejsie UIKit przez Swift: github.com/jameswomack/kvo-in-swift
james_womack
@JanDvorak Zobacz przewodnik programowania KVO , który jest miłym wprowadzeniem do tematu.
Rob
1
Chociaż nie jest to odpowiedź na twoje pytanie, możesz również rozpocząć działania za pomocą funkcji didset ().
Vincent
Uwaga jest Swift4 błędów podczas korzystania .initial. Rozwiązanie znajdziesz tutaj . Gorąco polecam zapoznać się z dokumentacją Apple . Został niedawno zaktualizowany i zawiera wiele ważnych uwag. Zobacz także inną odpowiedź
Honey

Odpowiedzi:

107

(Edytowano w celu dodania nowych informacji): zastanów się, czy użycie frameworka Combine może pomóc Ci osiągnąć to, czego chciałeś, zamiast używania KVO

Tak i nie. KVO działa na podklasach NSObject tak jak zawsze. Nie działa dla klas, które nie są podklasami NSObject. Swift nie ma (przynajmniej obecnie) własnego natywnego systemu obserwacji.

(Zobacz komentarze, jak ujawnić inne właściwości jako ObjC, aby KVO na nich działało)

Pełny przykład można znaleźć w dokumentacji Apple .

Catfish_Man
źródło
74
Od Xcode 6 beta 5 możesz użyć dynamicsłowa kluczowego w dowolnej klasie Swift, aby włączyć obsługę KVO.
fabb
7
Brawo dla @fabb! Dla jasności dynamicsłowo kluczowe trafia do właściwości, którą chcesz, aby klucz-wartość był obserwowalny.
Jerry
5
Wyjaśnienie dynamicsłowa kluczowego można znaleźć w sekcji Korzystanie z języka Swift with Cocoa i Objective-C w bibliotece programistów Apple .
Imanou Petit
6
Ponieważ nie było to dla mnie jasne na podstawie komentarza @ fabb: użyj dynamicsłowa kluczowego dla wszelkich właściwości wewnątrz klasy, które chcesz, aby były zgodne z KVO (a nie dynamicsłowa kluczowego na samej klasie). To zadziałało dla mnie!
Tim Camber
1
Nie całkiem; nie możesz zarejestrować nowego didSetu „z zewnątrz”, musi on być częścią tego typu w czasie kompilacji.
Catfish_Man
155

Możesz używać KVO w Swift, ale tylko dla dynamicwłaściwości NSObjectpodklasy. Weź pod uwagę, że chciałeś obserwować barwłaściwość Fooklasy. W Swift 4 określ barjako dynamicwłaściwość w swojej NSObjectpodklasie:

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

Następnie możesz się zarejestrować, aby obserwować zmiany w barnieruchomości. W Swift 4 i Swift 3.2 zostało to znacznie uproszczone, jak opisano w Korzystanie z obserwacji wartości kluczowej w języku 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)")
        }
    }
}

Zauważ, że w Swift 4 mamy teraz silne wpisywanie keypaths przy użyciu znaku ukośnika odwrotnego ( \.barjest to keypath dla barwłaściwości obserwowanego obiektu). Ponadto, ponieważ używa on wzorca zamykania zakończenia, nie musimy ręcznie usuwać obserwatorów (gdy tokenwypadnie poza zakres, obserwator jest usuwany za nas) ani nie musimy martwić się o wywołanie superimplementacji, jeśli klucz nie mecz. Zamknięcie jest wywoływane tylko wtedy, gdy wywoływany jest ten konkretny obserwator. Aby uzyskać więcej informacji, zobacz wideo WWDC 2017, Co nowego w programie Foundation .

Aby to zaobserwować, w Swift 3 jest to nieco bardziej skomplikowane, ale bardzo podobne do tego, co robi się w Objective-C. Mianowicie zaimplementowałbyś to, observeValue(forKeyPath keyPath:, of object:, change:, context:)co (a) upewnia się, że mamy do czynienia z naszym kontekstem (a nie czymś, do superczego zarejestrowała się nasza instancja); a następnie (b) superw razie potrzeby obsłuż go lub przekaż do implementacji. I pamiętaj, aby w razie potrzeby usunąć się jako obserwator. Na przykład możesz usunąć obserwatora, gdy zostanie zwolniony:

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

}

Zauważ, że możesz obserwować tylko właściwości, które mogą być reprezentowane w Objective-C. W związku z tym nie można obserwować structtypów ogólnych, typów Swift, enumtypów Swift itp.

Omówienie implementacji Swift 2 znajduje się poniżej, w mojej oryginalnej odpowiedzi.


Używanie dynamicsłowa kluczowego w celu osiągnięcia KVO z NSObjectpodklasami jest opisane w sekcji Obserwowanie wartości kluczowej rozdziału Adopting Cocoa Design Conventions w przewodniku Using Swift with Cocoa and Objective-C :

Obserwacja klucz-wartość to mechanizm, który umożliwia powiadamianie obiektów o zmianach określonych właściwości innych obiektów. Obserwacji klucz-wartość można używać z klasą Swift, o ile klasa dziedziczy po NSObjectklasie. Możesz użyć tych trzech kroków, aby wdrożyć obserwację klucz-wartość w języku Swift.

  1. Dodaj dynamicmodyfikator do dowolnej właściwości, którą chcesz obserwować. Aby uzyskać więcej informacji dynamic, zobacz Wymaganie dynamicznej wysyłki .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
  2. Utwórz globalną zmienną kontekstową.

    private var myContext = 0
  3. Dodaj obserwatora dla ścieżki klucza i zastąp observeValueForKeyPath:ofObject:change:context:metodę oraz usuń obserwatora w 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)
        }
    }

[Uwaga, ta dyskusja na temat KVO została następnie usunięta z przewodnika Używanie języka Swift z kakao i celem-C , który został dostosowany do języka Swift 3, ale nadal działa tak, jak opisano na początku tej odpowiedzi.]


Warto zauważyć, że Swift ma swój własny natywny system obserwacji właściwości , ale dotyczy to klasy określającej własny kod, który zostanie wykonany po obserwacji jej własnych właściwości. Z drugiej strony KVO jest przeznaczony do rejestracji w celu obserwowania zmian pewnej właściwości dynamicznej innej klasy.

Obrabować
źródło
Jaki jest cel myContexti jak obserwujesz wiele właściwości?
devth
1
Zgodnie z Przewodnikiem programowania KVO : „Kiedy rejestrujesz obiekt jako obserwator, możesz również podać contextwskaźnik. contextWskaźnik jest podawany obserwatorowi, gdy observeValueForKeyPath:ofObject:change:context:jest wywoływany. contextWskaźnik może być wskaźnikiem C lub odniesieniem do obiektu. contextWskaźnik może być używany jako niepowtarzalny identyfikator do określenia obserwowanej zmiany lub do dostarczenia obserwatorowi innych danych. "
Rob
musisz usunąć obserwatora w deinit
Jacky
3
@devth, jak rozumiem, jeśli podklasa lub nadklasa rejestruje również obserwatora KVO dla tej samej zmiennej, followValueForKeyPath zostanie wywołana wiele razy. Kontekst może służyć do rozróżnienia własnych powiadomień w tej sytuacji. Więcej na ten temat: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey
1
Jeśli zostawisz optionspuste, oznacza to po prostu, że changenie będzie zawierać starej ani nowej wartości (np. Możesz po prostu uzyskać nową wartość samodzielnie, odwołując się do samego obiektu). Jeśli po prostu podasz, .newa nie .old, oznacza to, że changebędzie zawierać tylko nową wartość, ale nie będzie starą (np. Często nie przejmujesz się tym, jaka była stara wartość, a tylko przejmujesz się nową). Jeśli chcesz observeValueForKeyPathprzekazać zarówno starą, jak i nową wartość, określ [.new, .old]. Podsumowując, optionspo prostu określa, co jest zawarte w changesłowniku.
Rob
92

Zarówno tak, jak i nie:

  • Tak , możesz używać tych samych starych interfejsów API KVO w Swift do obserwowania obiektów Objective-C.
    Możesz także obserwować dynamicwłaściwości obiektów Swift dziedziczących po NSObject.
    Ale ... Nie, to nie jest mocno wpisane, jak można by oczekiwać, że będzie to rodzimy system obserwacji Swift.
    Używanie Swift z Cocoa i Objective-C | Obserwacja kluczowych wartości

  • Nie , obecnie nie ma wbudowanego systemu obserwacji wartości dla dowolnych obiektów Swift.

  • Tak , istnieją wbudowane obserwatory właściwości , które są silnie wpisane.
    Ale ... Nie , to nie są KVO, ponieważ pozwalają jedynie na obserwację własnych właściwości obiektów, nie obsługują zagnieżdżonych obserwacji („ścieżek klucza”) i trzeba je jawnie zaimplementować.
    Swift Programming Language | Obserwatorzy właściwości

  • Tak , możesz zaimplementować jawne obserwowanie wartości, które będzie silnie wpisane i pozwoli na dodawanie wielu programów obsługi z innych obiektów, a nawet obsługuje zagnieżdżanie / „ścieżki klucza”.
    Ale ... Nie, to nie będzie KVO, ponieważ będzie działać tylko dla właściwości, które zaimplementujesz jako obserwowalne.
    Bibliotekę do realizacji takich obserwacji wartości znajdziesz tutaj:
    Observable-Swift - KVO for Swift - Value Observing and Events

slazyk
źródło
10

Przykład może trochę pomóc. Jeśli mam instancję modelklasy Modelz atrybutami namei statemogę obserwować te atrybuty za pomocą:

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)

Zmiany tych właściwości spowodują wywołanie:

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

        println("CHANGE OBSERVED: \(change)")
}
Paul Robinson
źródło
2
Jeśli się nie mylę, podejście do wywołania observValueForKeyPath jest dla Swift2.
Fattie
9

Tak.

KVO wymaga dynamicznego wysyłania, więc wystarczy dodać dynamicmodyfikator do metody, właściwości, indeksu lub inicjatora:

dynamic var foo = 0

W dynamicmodyfikujące zapewnia, że odniesienia do deklaracji będą wysyłane dynamicznie i dostępne za pośrednictwem objc_msgSend.

Bryan Luby
źródło
7

Oprócz odpowiedzi Roba. Ta klasa musi dziedziczyć po NSObject, a mamy 3 sposoby wywołania zmiany właściwości

Użyj setValue(value: AnyObject?, forKey key: String)zNSKeyValueCoding

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

Użyj willChangeValueForKeyi didChangeValueForKeyodNSKeyValueObserving

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

Użyj dynamic. Zobacz Zgodność typów Swift

Możesz również użyć dynamicznego modyfikatora, aby wymagać, aby dostęp do elementów członkowskich był dynamicznie przydzielany przez środowisko wykonawcze Objective-C, jeśli używasz interfejsów API, takich jak obserwacja klucz-wartość, która dynamicznie zastępuje implementację metody.

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

A metoda pobierająca i ustawiająca właściwości jest wywoływana, gdy jest używana. Możesz zweryfikować podczas współpracy z KVO. To jest przykład obliczonej właściwości

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
źródło
5

Przegląd

Jest to możliwe Combinebez użycia NSObjectlubObjective-C

Dostępność: iOS 13.0+ , macOS 10.15+, tvOS 13.0+, watchOS 6.0+, Mac Catalyst 13.0+,Xcode 11.0+

Uwaga: Musi być używany tylko z klasami, które nie mają typów wartości.

Kod:

Szybka wersja: 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

Wynik:

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

Odnosić się:

user1046037
źródło
4

Obecnie Swift nie obsługuje żadnego wbudowanego mechanizmu obserwacji zmian właściwości obiektów innych niż „self”, więc nie, nie obsługuje KVO.

Jednak KVO jest tak podstawową częścią Objective-C i Cocoa, że ​​wydaje się całkiem prawdopodobne, że zostanie dodana w przyszłości. Obecna dokumentacja wydaje się sugerować, że:

Obserwacja klucz-wartość

Informacje w przygotowaniu.

Używanie Swift z Cocoa i Objective-C

ColinE
źródło
2
Oczywiście ten przewodnik, do którego teraz się odwołujesz, opisuje, jak zrobić KVO w Swift.
Rob
4
Tak, teraz wdrożone od września 2014
Max MacLeod,
4

Jedną ważną rzeczą, o której warto wspomnieć, jest to, że po aktualizacji Xcode do wersji 7 beta może pojawić się następujący komunikat: „Metoda nie zastępuje żadnej metody ze swojej nadklasy” . Dzieje się tak z powodu opcjonalności argumentów. Upewnij się, że program obsługi obserwacji wygląda dokładnie tak:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)
Arthur Gevorkyan
źródło
2
W Xcode beta 6 wymaga: override func observValueForKeyPath (keyPath: String ?, ofObject object: AnyObject ?, change: [String: AnyObject] ?, context: UnsafeMutablePointer <Void>)
hcanfly
4

Może to być pomocne dla kilku osób -

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

Użyłem KVO w ten sposób w Swift 3. Możesz użyć tego kodu z kilkoma zmianami.

Nikhil Manapure
źródło
1

Inny przykład dla każdego, kto ma problem z typami takimi jak Int? i CGFloat ?. Po prostu ustawiasz swoją klasę jako podklasę NSObject i deklarujesz swoje zmienne w następujący sposób, np .:

class Theme : NSObject{

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

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

}
DrPatience
źródło