Comprobando si un valor se cambia usando KVO en Swift 3
swift3 key-value-observing (2)
Me gustaría saber cuándo cambia un conjunto de propiedades de un objeto Swift. Anteriormente, había implementado esto en Objective-C, pero tengo algunas dificultades para convertirlo a Swift.
Mi código anterior de Objective-C es:
- (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
if (![change[@"new"] isEqual:change[@"old"]])
[self edit];
}
Mi primer paso en una solución Swift fue:
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()
}
}
Sin embargo, el compilador se queja: "El operador binario ''! ='' No se puede aplicar a dos ''¿Cualquiera?'' operandos
Mi segundo intento:
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()
}
}
}
}
Pero, al pensar en esto, no creo que esto funcione para primitivas de objetos rápidos como Int, que (supongo) no heredan de NSObject y, a diferencia de la versión Objective-C, no se incluirán en NSNumber cuando se coloquen en El diccionario de cambio.
Entonces, la pregunta es ¿cómo hago la tarea aparentemente fácil de determinar si un valor se está cambiando realmente usando el KVO en Swift3?
Además, pregunta adicional, ¿cómo hago uso de la variable ''de objeto''? No me deja cambiar el nombre y, por supuesto, no le gustan las variables con espacios en ellos.
A continuación se encuentra mi respuesta original de Swift 3, pero Swift 4 simplifica el proceso, eliminando la necesidad de cualquier casting. Por ejemplo, si está observando la propiedad Int
denominada bar
del objeto 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() { ... }
}
Tenga en cuenta, este enfoque basado en el cierre:
Le
observeValue
necesidad de implementar un método separado deobserveValue
;Elimina la necesidad de especificar un
context
y verificar ese contexto; yEl
change.newValue
ychange.oldValue
están correctamente escritos, lo que elimina la necesidad de realizar un casting manual. Si la propiedad era opcional, es posible que tenga que desenvolverlos de manera segura, pero no se necesita un casting.
Lo único que debe tener cuidado es asegurarse de que su cierre no introduzca un ciclo de referencia fuerte (de ahí el uso del patrón [weak self]
).
Mi respuesta original de Swift 3 está abajo.
Tu dijiste:
Pero, al pensar en esto, no creo que esto funcione para primitivas de objetos rápidos como
Int
que (supongo) no heredan deNSObject
y, a diferencia de la versión Objective-C, no seNSNumber
enNSNumber
cuando se coloquen en El diccionario de cambio.
En realidad, si miras esos valores, si la propiedad observada es un Int
, sí viene a través del diccionario como un NSNumber
.
Entonces, puedes quedarte en el mundo 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()
}
}
O usarlos como 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()
}
}
O, si este fuera un valor Int
de alguna propiedad dynamic
de alguna clase Swift, seguiría y los Int
como 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 preguntaste:
Además, pregunta extra, ¿cómo hago uso de la variable
of object
? No me deja cambiar el nombre y, por supuesto, no le gustan las variables con espacios en ellos.
La etiqueta es la etiqueta externa para este parámetro (se usa cuando se llama a este método; en este caso, el sistema operativo nos llama esto a nosotros, por lo que no usamos esta etiqueta externa antes de la firma del método). El object
es la etiqueta interna (utilizada dentro del propio método). Swift ha tenido la capacidad de etiquetas externas e internas para los parámetros por un tiempo, pero solo ha sido realmente aceptado en la API a partir de Swift 3.
En términos de cuándo usa este parámetro de change
, lo usa si está observando las propiedades de más de un objeto, y si estos objetos necesitan un manejo diferente en el KVO, por ejemplo:
foo.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)
baz.addObserver(self, forKeyPath: #keyPath(Foo.qux), options: [.new, .old], context: &observerContext)
Y entonces:
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
}
}
Como punto aparte, generalmente recomiendo usar el context
, por ejemplo, tener una private var
:
private var observerContext = 0
Y luego agrega el observador usando ese contexto:
foo.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)
Y luego haga que mi valor de observeValue
asegure de que sea su context
, y no uno establecido por su superclase:
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()
}
}
Mi "segundo intento" original contiene un error que no se puede detectar cuando un valor cambia ao desde nil. El primer intento se puede arreglar realmente una vez que uno se da cuenta de que todos los valores de "cambio" son 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()
}
}
o una versión más concisa si prefieres:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if change?[.newKey] as? NSObject != change?[.oldKey] as? NSObject {
edit()
}
}