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 context pointer, and forward unknown keys to super.

  • 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.