L'osservazione del valore-chiave (KVO) è disponibile in Swift?


174

In tal caso, ci sono differenze chiave che altrimenti non erano presenti quando si utilizza l'osservazione del valore-chiave in Objective-C?


2
Un progetto di esempio che dimostra che KVO viene utilizzato in un'interfaccia UIKit tramite Swift: github.com/jameswomack/kvo-in-swift
james_womack il

@JanDvorak Vedi la Guida alla programmazione KVO , che è una bella introduzione all'argomento.
Rob,

1
Sebbene non sia una risposta alla tua domanda, puoi anche avviare azioni usando la funzione didset ().
Vincent,

Nota che c'è un bug Swift4 quando lo usi .initial. Per una soluzione vedi qui . Consiglio vivamente di consultare i documenti Apple . È stato aggiornato di recente e copre molte note importanti. Vedi anche l' altra risposta di
Honey,

Risposte:


107

(Modificato per aggiungere nuove informazioni): considera se usare il framework Combine può aiutarti a realizzare ciò che volevi, piuttosto che usare KVO

Sì e no. KVO funziona su sottoclassi di NSObject come ha sempre fatto. Non funziona per le classi che non eseguono la sottoclasse di NSObject. Swift non ha (almeno attualmente) un proprio sistema di osservazione nativo.

(Vedi i commenti su come esporre altre proprietà come ObjC in modo che KVO lavori su di esse)

Consulta la documentazione Apple per un esempio completo.


74
Da Xcode 6 beta 5 è possibile utilizzare la dynamicparola chiave su qualsiasi classe Swift per abilitare il supporto KVO.
Fabb,

7
Evviva @fabb! Per chiarezza, la dynamicparola chiave va sulla proprietà che si desidera rendere osservabile valore-chiave.
Jerry,

5
La spiegazione della dynamicparola chiave può essere trovata nella sezione Utilizzo di Swift con Cocoa e Objective-C della Biblioteca per sviluppatori Apple .
Imanou Petit,

6
Dal momento che questo non mi era chiaro dal commento di @ fabb: usa la dynamicparola chiave per qualsiasi proprietà all'interno di una classe che desideri sia conforme a KVO (non la dynamicparola chiave sulla classe stessa). Questo ha funzionato per me!
Tim Camber,

1
Non proprio; non è possibile registrare un nuovo didSet dall'esterno, deve essere parte di quel tipo al momento della compilazione.
Catfish_Man,

155

Puoi usare KVO in Swift, ma solo per le dynamicproprietà della NSObjectsottoclasse. Considera che volevi osservare la barproprietà di una Fooclasse. In Swift 4, specifica barcome dynamicproprietà nella NSObjectsottoclasse:

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

È quindi possibile registrarsi per osservare le modifiche alla barproprietà. In Swift 4 e Swift 3.2, questo è stato notevolmente semplificato, come indicato in Uso dell'osservazione dei valori-chiave in 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)")
        }
    }
}

Nota, in Swift 4, ora abbiamo una forte tipizzazione di keypath usando il carattere barra rovesciata ( \.barè il keypath per la barproprietà dell'oggetto che si sta osservando). Inoltre, poiché utilizza il modello di chiusura del completamento, non è necessario rimuovere manualmente gli osservatori (quando il tokencampo di applicazione non rientra, l'osservatore viene rimosso per noi) né dobbiamo preoccuparci di chiamare l' superimplementazione se la chiave non lo fa incontro. La chiusura viene chiamata solo quando viene invocato questo particolare osservatore. Per ulteriori informazioni, vedere il video del WWDC 2017, Novità nella Fondazione .

In Swift 3, osservare questo, è un po 'più complicato, ma molto simile a quello che si fa in Objective-C. Vale a dire, implementeresti observeValue(forKeyPath keyPath:, of object:, change:, context:)quale (a) assicurati che abbiamo a che fare con il nostro contesto (e non qualcosa che la nostra superistanza si era registrata per osservare); e poi (b) gestirlo o passarlo superall'implementazione, se necessario. E assicurati di rimuoverti come osservatore quando appropriato. Ad esempio, è possibile rimuovere l'osservatore quando viene deallocato:

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

}

Nota, puoi solo osservare le proprietà che possono essere rappresentate in Objective-C. Pertanto, non è possibile osservare generici, structtipi Swift enum, tipi Swift , ecc.

Per una discussione sull'implementazione di Swift 2, vedi la mia risposta originale, di seguito.


L'uso della dynamicparola chiave per ottenere KVO con le NSObjectsottoclassi è descritto nella sezione Osservazione dei valori-chiave del capitolo Adozione di convenzioni di progettazione del cacao della guida Uso di Swift con cacao e Objective-C :

L'osservazione del valore-chiave è un meccanismo che consente agli oggetti di ricevere notifiche delle modifiche alle proprietà specificate di altri oggetti. Puoi utilizzare l'osservazione del valore-chiave con una classe Swift, purché la classe erediti dalla NSObjectclasse. Puoi utilizzare questi tre passaggi per implementare l'osservazione del valore-chiave in Swift.

  1. Aggiungi il dynamicmodificatore a qualsiasi proprietà che desideri osservare. Per ulteriori informazioni su dynamic, vedere Richiesta di invio dinamico .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
  2. Crea una variabile di contesto globale.

    private var myContext = 0
  3. Aggiungi un osservatore per il percorso chiave, sovrascrivi il observeValueForKeyPath:ofObject:change:context:metodo e rimuovi l'osservatore 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)
        }
    }

[Nota, questa discussione KVO è stata successivamente rimossa dalla guida Uso di Swift con Cocoa e Objective-C , che è stata adattata per Swift 3, ma funziona ancora come indicato all'inizio di questa risposta.]


Vale la pena notare che Swift ha il proprio sistema di osservazione delle proprietà native , ma questo è per una classe che specifica il proprio codice che verrà eseguito all'osservazione delle proprie proprietà. KVO, d'altra parte, è progettato per registrarsi per osservare le modifiche ad alcune proprietà dinamiche di altre classi.


Qual è lo scopo myContexte come osservi più proprietà?
Devth,

1
Secondo la Guida alla programmazione KVO : "Quando registri un oggetto come osservatore, puoi anche fornire un contextpuntatore. Il contextpuntatore viene fornito all'osservatore quando observeValueForKeyPath:ofObject:change:context:viene invocato. Il contextpuntatore può essere un puntatore C o un riferimento a un oggetto. Il contextpuntatore può essere utilizzato come identificatore univoco per determinare la modifica osservata o per fornire altri dati all'osservatore. "
Rob,

è necessario rimuovere l'osservatore in deinit
Jacky,

3
@devth, a quanto ho capito, se la sottoclasse o la superclasse registra anche l'osservatore KVO per la stessa variabile, observValueForKeyPath verrà chiamato più volte. Il contesto può essere utilizzato per distinguere le proprie notifiche in questa situazione. Altre informazioni: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey

1
Se lasci optionsvuoto, significa semplicemente che changenon includerà il vecchio o il nuovo valore (ad es. Potresti semplicemente ottenere il nuovo valore facendo riferimento all'oggetto stesso). Se specifichi .newe no .old, significa che changeincluderà solo il nuovo valore, ma non il vecchio valore (ad esempio, spesso non ti interessa quale fosse il vecchio valore, ma ti interessa solo il nuovo valore). Se devi observeValueForKeyPathpassare sia il vecchio che il nuovo valore, quindi specifica [.new, .old]. In conclusione, optionsspecifica solo cosa è incluso nel changedizionario.
Rob,

92

Sia sì che no:

  • , puoi utilizzare le stesse vecchie API KVO in Swift per osservare gli oggetti Objective-C.
    Puoi anche osservare le dynamicproprietà degli oggetti Swift che ereditano NSObject.
    Ma ... No , non è fortemente tipizzato come ci si potrebbe aspettare dal sistema di osservazione nativo di Swift.
    Utilizzo di Swift con Cocoa e Objective-C | Osservazione del valore chiave

  • No , attualmente non esiste un sistema di osservazione del valore incorporato per oggetti Swift arbitrari.

  • , ci sono osservatori di proprietà incorporati , che sono fortemente tipizzati.
    Ma ... No, non sono KVO, poiché consentono solo l'osservazione delle proprietà degli oggetti, non supportano le osservazioni nidificate ("percorsi chiave") e devi implementarle esplicitamente.
    Il linguaggio di programmazione Swift | Osservatori immobiliari

  • , è possibile implementare l'osservazione esplicita del valore, che sarà fortemente tipizzata, e consentire l'aggiunta di più gestori da altri oggetti e persino supportare l'annidamento / "percorsi chiave".
    Ma ... No , non sarà KVO poiché funzionerà solo per le proprietà che implementerai come osservabili.
    È possibile trovare una libreria per implementare tale osservazione del valore qui:
    Observable-Swift - KVO per Swift - Osservazione del valore ed eventi


10

Un esempio potrebbe aiutare un po 'qui. Se ho un'istanza modeldi classe Modelcon attributi namee stateposso osservare quegli attributi con:

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)

Le modifiche a queste proprietà attiveranno una chiamata a:

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

        println("CHANGE OBSERVED: \(change)")
}

2
Se non sbaglio, l'approccio di chiamata observValueForKeyPath è per Swift2.
Fattie,

9

Sì.

KVO richiede l'invio dinamico, quindi devi semplicemente aggiungere il dynamicmodificatore a un metodo, proprietà, pedice o inizializzatore:

dynamic var foo = 0

Il dynamicmodificatore garantisce che i riferimenti alla dichiarazione vengano spediti e accessibili dinamicamente objc_msgSend.


7

Oltre alla risposta di Rob. Quella classe deve ereditare NSObjecte abbiamo 3 modi per innescare il cambio di proprietà

Utilizzare setValue(value: AnyObject?, forKey key: String)daNSKeyValueCoding

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

Usa willChangeValueForKeye didChangeValueForKeydaNSKeyValueObserving

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

Usa dynamic. Vedi Compatibilità del tipo Swift

È inoltre possibile utilizzare il modificatore dinamico per richiedere che l'accesso ai membri venga inviato in modo dinamico tramite il runtime Objective-C se si utilizzano API come valore-chiave osservando che sostituiscono in modo dinamico l'implementazione di un metodo.

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

E il getter e il setter di proprietà vengono chiamati quando utilizzati. Puoi verificare quando lavori con KVO. Questo è un esempio di proprietà calcolata

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

5

Panoramica

È possibile usare Combinesenza usare NSObjectoObjective-C

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

Nota: deve essere utilizzato solo con classi non con tipi di valore.

Codice:

Versione 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

Produzione:

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

Fare riferimento:


4

Attualmente Swift non supporta alcun meccanismo incorporato per osservare i cambiamenti di proprietà di oggetti diversi da "self", quindi no, non supporta KVO.

Tuttavia, KVO è una parte così fondamentale di Objective-C e Cocoa che sembra abbastanza probabile che verrà aggiunto in futuro. L'attuale documentazione sembra implicare questo:

Osservazione di valori-chiave

Informazioni in arrivo.

Utilizzo di Swift con Cocoa e Objective-C


2
Ovviamente, quella guida a cui fai riferimento ora descrive come eseguire KVO in Swift.
Rob,

4
Sì, ora implementato da settembre 2014
Max MacLeod

4

Una cosa importante da ricordare è che dopo aver aggiornato il tuo Xcode a 7 beta potresti ricevere il seguente messaggio: "Il metodo non sostituisce alcun metodo dalla sua superclasse" . Questo a causa dell'opzionalità degli argomenti. Assicurati che il gestore dell'osservazione sia esattamente il seguente:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)

2
In Xcode beta 6 è necessario: sovrascrivere la funzione observ observValueForKeyPath (keyPath: String ?, ofObject object: AnyObject ?, change: [String: AnyObject] ?, contesto: UnsafeMutablePointer <Void>)
hcanfly

4

Questo può rivelarsi utile per poche persone -

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

Avevo usato KVO in questo modo in Swift 3. Puoi usare questo codice con poche modifiche.


1

Un altro esempio per chiunque incontri un problema con tipi come Int? e CGFloat ?. Devi semplicemente impostare la tua classe come sottoclasse di NSObject e dichiarare le tue variabili come segue, ad esempio:

class Theme : NSObject{

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

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

}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.