Block Based KVO
ID |
swift.block_based_kvo |
Severity |
low |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Best Practice |
Language |
Swift |
Tags |
best-practice, ios, modernization |
Description
Reports use of the legacy NSObject KVO API:
-
an override of
observeValue(forKeyPath:of:change:context:), or -
a call to
addObserver(_:forKeyPath:options:context:).
Both predate the closure-based observe(_:options:changeHandler:)
API introduced in Swift 4.
override func observeValue(forKeyPath keyPath: String?, // FLAW
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) { /* ... */ }
obj.addObserver(self, forKeyPath: "name", options: .new, context: nil) // FLAW
let obs = obj.observe(\.name, options: .new) { obj, change in /* ... */ } // OK
Rationale
The string-keyed KVO API is unsafe and clumsy:
-
Key paths are passed as
String?and only checked at runtime, so a typo silently disables the observer. -
You must manually distinguish your observations from a superclass’s via the opaque
contextpointer, and forward unknown keys tosuper. -
Removal is asymmetric: forgetting
removeObserver(_:forKeyPath:)crashes the process when the observed object is later released.
The block-based observe(_:options:changeHandler:) API replaces these
sharp edges with strongly-typed KeyPath values, a returned
NSKeyValueObservation token that invalidates itself in deinit, and
inline closures that capture only what they need.
Remediation
Adopt the closure-based API and hold the returned observation token in a property:
class ViewModel {
private var nameObservation: NSKeyValueObservation?
func bind(to model: Model) {
nameObservation = model.observe(\.name, options: [.new, .initial]) { model, change in
print("name is now \(model.name)")
}
}
}
For Combine-based code, KeyValueObservingPublisher (publisher(for:))
is the streaming equivalent.