Checking if a value is changed using KVO in Swift 3
key-value-observing, swift, swift3
Solution
Below is my original Swift 3 answer, but Swift 4 simplifies the process, eliminating the need for any casting. For example, if you are observing the `Int` property called `bar` of the `foo` object:
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() { ... }
}
Note, this closure based approach:
Gets you out of needing to implement a separate `observeValue` method;
Eliminates the need for specifying a `context` and checking that context; and
The `change.newValue` and `change.oldValue` are properly typed, eliminating the need for manual casting. If the property was an optional, you may have to safely unwrap them, but no casting is needed.
The only thing you need to be careful about is making sure your closure doesn't introduce a strong reference cycle (hence the use of `[weak self]` pattern).
My original Swift 3 answer is below.
You said:
But, in thinking about this, I don't think this will work for primitives of swift objects such as `Int` which (I assume) do not inherit from `NSObject` and unlike the Objective-C version won't be boxed into `NSNumber` when placed into the change dictionary.
Actually, if you look at those values, if the observed property is an `Int`, it does come through the dictionary as a `NSNumber`.
So, you can either stay in the `NSObject` world:
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()
}
}
Or use them as `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()
}
}
Or, I'd if this was an `Int` value of some `dynamic` property of some Swift class, I'd go ahead and cast them as `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()
}
}
You asked:
Also, bonus question, how do I make use of the `of object` variable? It won't let me change the name and of course doesn't like variables with spaces in them.
The `of` is the external label for this parameter (used when if you were calling this method; in this case, the OS calls this for us, so we don't use this external label short of in the method signature). The `object` is the internal label (used within the method itself). Swift has had the capability for external and internal labels for parameters for a while, but it's only been truly embraced in the API as of Swift 3.
In terms of when you use this `change` parameter, you use it if you're observing the properties of more than one object, and if these objects need different handling on the KVO, e.g.:
foo.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)
baz.addObserver(self, forKeyPath: #keyPath(Foo.qux), options: [.new, .old], context: &observerContext)
And then:
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
}
}
As an aside, I'd generally recommend using the `context`, e.g., have a `private var`:
private var observerContext = 0
And then add the observer using that context:
foo.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)
And then have my `observeValue` make sure it was its `context`, and not one established by its superclass:
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()
}
}
Problem
I would like to know when a set of properties of a Swift object changes. Previously, I had implemented this in Objective-C, but I'm having some difficulty converting it to Swift. My previous Objective-C code is: ``` - (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { if (![change[@"new"] isEqual:change[@"old"]]) [self edit]; } ``` My first pass at a Swift solution was: ``` 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() } } ``` However, the compiler complains: "Binary operator '!=' cannot be applied to two 'Any?' operands" My second attempt: ``` 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() } } } } ``` But, in thinking about this, I don't think this will work for primitives of swift objects such as Int which (I assume) do not inherit from NSObject and unlike the Objective-C version won't be boxed into NSNumber when placed into the change dictionary. So, the question is how do I do the seemingly easy task of determining if a value is actually being changed using the KVO in Swift3? Also, bonus question, how do I make use of the 'of object' variable? It won't let me change the name and of course doesn't like variables with spaces in them.