¿La observación de valores clave (KVO) está disponible en Swift?

174

Si es así, ¿hay alguna diferencia clave que de otro modo no estaba presente al usar la observación de valor clave en Objective-C?

codeperson
fuente
2
Un proyecto de ejemplo que demuestra que KVO se está utilizando en una interfaz UIKit a través de Swift: github.com/jameswomack/kvo-in-swift
james_womack
@ JanDvorak Consulte la Guía de programación de KVO , que es una buena introducción al tema.
Rob
1
Aunque no es una respuesta a su pregunta, también puede iniciar acciones utilizando la función didset ().
Vincent
Tenga en cuenta que hay un error de Swift4 cuando lo usa .initial. Para una solución ver aquí . Recomiendo ver los documentos de Apple . Se ha actualizado recientemente y cubre muchas notas importantes. Ver también la otra respuesta de
Honey,

Respuestas:

107

(Editado para agregar nueva información): considere si usar el marco Combinar puede ayudarlo a lograr lo que quería, en lugar de usar KVO

Si y no. KVO funciona en las subclases de NSObject como siempre lo ha hecho. No funciona para clases que no subclase NSObject. Swift no tiene (al menos actualmente) su propio sistema de observación nativo.

(Vea los comentarios sobre cómo exponer otras propiedades como ObjC para que KVO trabaje en ellas)

Consulte la documentación de Apple para ver un ejemplo completo.

Catfish_Man
fuente
74
Desde Xcode 6 beta 5 puede usar la dynamicpalabra clave en cualquier clase Swift para habilitar el soporte de KVO.
fabb
77
¡Hurra por @fabb! Para mayor claridad, la dynamicpalabra clave va en la propiedad que desea que sea clave-valor-observable.
Jerry
55
La explicación de la dynamicpalabra clave se puede encontrar en la sección Uso de Swift con Cocoa y Objective-C de la Biblioteca para desarrolladores de Apple .
Imanou Petit
66
Como esto no me quedó claro por el comentario de @ fabb: use la dynamicpalabra clave para cualquier propiedad dentro de una clase que le gustaría cumplir con KVO (no la dynamicpalabra clave en la clase en sí). ¡Esto funcionó para mí!
Tim Camber
1
Realmente no; no puede registrar un nuevo didSet desde el "exterior", debe ser parte de ese tipo en tiempo de compilación.
Catfish_Man
155

Puede usar KVO en Swift, pero solo para dynamicpropiedades de NSObjectsubclase. Considere que desea observar la barpropiedad de una Fooclase. En Swift 4, especifique barcomo dynamicpropiedad en su NSObjectsubclase:

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

Luego puede registrarse para observar los cambios en la barpropiedad. En Swift 4 y Swift 3.2, esto se ha simplificado enormemente, como se describe en Uso de la observación de valores clave en 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)")
        }
    }
}

Tenga en cuenta que en Swift 4, ahora tenemos un tipeo fuerte de las rutas de teclado utilizando el carácter de barra diagonal inversa ( \.bares la ruta de acceso de la barpropiedad del objeto que se está observando). Además, debido a que está utilizando el patrón de cierre de finalización, no tenemos que eliminar manualmente los observadores (cuando la tokencaída está fuera del alcance, el observador se elimina por nosotros) ni tenemos que preocuparnos de llamar a la superimplementación si la clave no partido. El cierre se llama solo cuando se invoca a este observador en particular. Para obtener más información, vea el video WWDC 2017, What's New in Foundation .

En Swift 3, para observar esto, es un poco más complicado, pero muy similar a lo que se hace en Objective-C. Es decir, implementaría observeValue(forKeyPath keyPath:, of object:, change:, context:)qué (a) se asegura de que estemos tratando con nuestro contexto (y no con algo que nuestra superinstancia haya registrado para observar); y luego (b) manejarlo o pasarlo a la superimplementación, según sea necesario. Y asegúrese de retirarse como observador cuando sea apropiado. Por ejemplo, puede eliminar el observador cuando se desasigna:

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

}

Tenga en cuenta que solo puede observar propiedades que se pueden representar en Objective-C. Por lo tanto, no puede observar genéricos, structtipos Swift enum, tipos Swift , etc.

Para una discusión sobre la implementación de Swift 2, vea mi respuesta original, a continuación.


El uso de la dynamicpalabra clave para lograr KVO con NSObjectsubclases se describe en la sección Observación de valores clave del capítulo Adopción de convenciones de diseño de cacao de la guía Uso rápido con cacao y Objetivo-C :

La observación de valores clave es un mecanismo que permite que los objetos sean notificados de los cambios en las propiedades especificadas de otros objetos. Puede usar la observación de valores clave con una clase Swift, siempre que la clase herede de la NSObjectclase. Puede utilizar estos tres pasos para implementar la observación de valores clave en Swift.

  1. Agregue el dynamicmodificador a cualquier propiedad que desee observar. Para obtener más información sobre dynamic, consulte Requerimiento de envío dinámico .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
  2. Crea una variable de contexto global.

    private var myContext = 0
  3. Agregue un observador para la ruta de acceso clave, anule el observeValueForKeyPath:ofObject:change:context:método y elimine el observador 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)
        }
    }

[Tenga en cuenta que esta discusión de KVO se ha eliminado posteriormente de la guía Uso de Swift con Cocoa y Objective-C , que se ha adaptado para Swift 3, pero aún funciona como se describe en la parte superior de esta respuesta.]


Vale la pena señalar que Swift tiene su propio sistema de observación de propiedades nativas , pero eso es para una clase que especifica su propio código que se realizará al observar sus propias propiedades. KVO, por otro lado, está diseñado para registrarse para observar cambios en alguna propiedad dinámica de otra clase.

Robar
fuente
¿Cuál es el propósito myContexty cómo observa múltiples propiedades?
devth
1
Según la Guía de programación de KVO : "Cuando registra un objeto como observador, también puede proporcionar un contextpuntero. El contextpuntero se proporciona al observador cuando observeValueForKeyPath:ofObject:change:context:se invoca. El contextpuntero puede ser un puntero C o una referencia de objeto. El contextpuntero puede ser se utiliza como un identificador único para determinar el cambio que se está observando o para proporcionar otros datos al observador ".
Rob
necesita quitar observador en deinit
Jacky
3
@devth, según tengo entendido, si la subclase o la superclase también registran el observador KVO para la misma variable, observeValueForKeyPath se llamará varias veces. El contexto se puede utilizar para distinguir las notificaciones propias en esta situación. Más sobre esto: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey
1
Si lo deja optionsvacío, solo significa que changeno incluirá el valor antiguo o nuevo (por ejemplo, puede obtener el nuevo valor usted mismo haciendo referencia al objeto mismo). Si solo especifica .newy no .old, significa que changeincluirá solo el nuevo valor, pero no el valor anterior (por ejemplo, a menudo no le importa cuál era el valor anterior, sino solo el nuevo valor). Si necesita observeValueForKeyPathpasarle el valor antiguo y el nuevo, especifique [.new, .old]. En optionspocas palabras, solo especifica lo que se incluye en el changediccionario.
Rob
92

Tanto sí como no:

  • , puede usar las mismas API antiguas de KVO en Swift para observar objetos Objective-C.
    También puede observar dynamicpropiedades de objetos Swift heredados de NSObject.
    Pero ... No , no está fuertemente tipado como se podría esperar que sea el sistema de observación nativo Swift.
    Usando Swift con cacao y Objective-C | Observación de valor clave

  • No , actualmente no existe un sistema de observación de valor incorporado para objetos Swift arbitrarios.

  • , hay observadores de propiedades incorporados , que están fuertemente tipados.
    Pero ... No, no son KVO, ya que solo permiten observar las propiedades propias de los objetos, no admiten observaciones anidadas ("rutas clave"), y debe implementarlas explícitamente.
    El lenguaje de programación Swift | Observadores de propiedades

  • , puede implementar la observación de valores explícitos, que se tipeará con fuerza, y permitirá agregar múltiples controladores de otros objetos, e incluso admitir anidamiento / "rutas clave".
    Pero ... No , no será KVO ya que solo funcionará para propiedades que implemente como observables.
    Puede encontrar una biblioteca para implementar dicho valor observando aquí:
    Observable-Swift - KVO para Swift - Observación de valores y eventos

slazyk
fuente
10

Un ejemplo podría ayudar un poco aquí. Si tengo una instancia modelde clase Modelcon atributos namey statepuedo observar esos atributos 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)

Los cambios en estas propiedades activarán una llamada a:

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

        println("CHANGE OBSERVED: \(change)")
}
Paul Robinson
fuente
2
Si no me equivoco, el enfoque de la llamada observeValueForKeyPath es para Swift2.
Fattie
9

Si.

KVO requiere despacho dinámico, por lo que simplemente necesita agregar el dynamicmodificador a un método, propiedad, subíndice o inicializador:

dynamic var foo = 0

El dynamicmodificador asegura que las referencias a la declaración se despacharán dinámicamente y se accederá a través de ellas objc_msgSend.

Bryan Luby
fuente
7

Además de la respuesta de Rob. Esa clase debe heredar de NSObject, y tenemos 3 formas de activar el cambio de propiedad

Usar setValue(value: AnyObject?, forKey key: String)desdeNSKeyValueCoding

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

Uso willChangeValueForKeyy didChangeValueForKeydeNSKeyValueObserving

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

Uso dynamic. Ver compatibilidad de tipo Swift

También puede usar el modificador dinámico para requerir que el acceso a los miembros se envíe dinámicamente a través del tiempo de ejecución de Objective-C si está utilizando API como la observación de valor clave que reemplaza dinámicamente la implementación de un método.

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

Y la propiedad getter y setter se llama cuando se usa. Puede verificar cuando trabaja con KVO. Este es un ejemplo de propiedad calculada

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

Visión general

Es posible usar Combinesin usar NSObjectoObjective-C

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

Nota: Debe usarse solo con clases que no sean con tipos de valor.

Código:

Versión 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

Salida:

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

Referir:

usuario1046037
fuente
4

Actualmente Swift no admite ningún mecanismo integrado para observar cambios de propiedad de objetos que no sean 'self', por lo que no, no admite KVO.

Sin embargo, KVO es una parte tan fundamental de Objective-C y Cocoa que parece bastante probable que se agregue en el futuro. La documentación actual parece implicar esto:

Observación de valor clave

Información próxima.

Usando Swift con cacao y Objective-C

ColinE
fuente
2
Obviamente, esa guía a la que hace referencia ahora describe cómo hacer KVO en Swift.
Rob
44
Sí, ahora implementado a partir de septiembre de 2014
Max MacLeod
4

Una cosa importante a mencionar es que después de actualizar su Xcode a 7 beta , puede recibir el siguiente mensaje: "El método no anula ningún método de su superclase" . Eso se debe a la opcionalidad de los argumentos. Asegúrese de que su controlador de observación tenga exactamente el siguiente aspecto:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)
Arthur Gevorkyan
fuente
2
En Xcode beta 6 requiere: anular func observeValueForKeyPath (keyPath: String ?, ofObject object: AnyObject ?, change: [String: AnyObject] ?, context: UnsafeMutablePointer <Void>)
hcanfly
4

Esto puede ser útil para algunas personas.

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

Había usado KVO de esta manera en Swift 3. Puede usar este código con pocos cambios.

Nikhil Manapure
fuente
1

¿Otro ejemplo para cualquiera que tenga un problema con tipos como Int? y CGFloat ?. Simplemente establece su clase como una subclase de NSObject y declara sus variables de la siguiente manera, por ejemplo:

class Theme : NSObject{

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

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

}
DrPatience
fuente