What does KVO do to an observed object at run time, and how can you see it?
Predict first
Two properties are observed, one dynamic and one not. What is logged?
final class Model: NSObject {
@objc dynamic var first = "Ada"
@objc var plain = 0
}
let m = Model()
nonisolated(unsafe) var log: [String] = []
let t1 = m.observe(\.first, options: [.new]) { _, c in log.append("first \(c.newValue ?? "-")") }
let t2 = m.observe(\.plain, options: [.new]) { _, c in log.append("plain \(c.newValue ?? -1)") }
m.first = "Grace"
m.plain = 5
print(log)
print(NSStringFromClass(object_getClass(m)!).contains("NSKVONotifying_"))
_ = (t1, t2)
- ["first Grace", "plain 5"] / false
- ["first Grace"] / true
- [] / true
Mental model. KVO swaps the building’s doorbell for one that also rings an announcement. If you walk in through a side window (a direct write), the announcement never fires.
When you observe an object, the runtime creates (once per class) a subclass named NSKVONotifying_Model and switches the instance’s isa to it with object_setClass. The subclass overrides each observed setter so that it calls willChangeValue(forKey:), the original setter, then didChangeValue(forKey:), which delivers the notification, and it overrides -class so the swap is hidden. Nothing is added to your code: the observed property is just a method the generated class can intercept. That is the same mechanism as the runtime-class question.
This is why the property must be @objc dynamic (the basics are in the earlier KVO question): a direct write bypasses the generated override and nothing fires, and the compiler warns that observing a non-dynamic property “may lead to unexpected behavior”. You can see the swap yourself: object_getClass reports the generated subclass while the object is observed. The modern API is observe(\.name, options:) { object, change in }, which returns an NSKeyValueObservation token: the observation lives exactly as long as you keep the token, and invalidate() (or releasing it) ends it. The old addObserver(_:forKeyPath:) has a context pointer and a removeObserver that must balance.
Only the dynamic property is intercepted. While observed, the instance’s real class is the generated NSKVONotifying_… subclass.
Common traps
- Observing a property that is not
dynamic. It compiles and silently never notifies. - Dropping the token. The observation ends when the returned
NSKeyValueObservationis released, so store it.
They might follow up with
- Does KVO work on a Swift struct property? No; it needs an
NSObjectinstance and the runtime. - When would you use Combine or Observation instead? For pure Swift models; KVO remains the answer for UIKit, AVFoundation and Foundation properties.
Related questions
- 2.4.5 How does KVO work in Swift, and why does it sometimes never fire?
- 4.1.4 What is isa, and why does your object’s runtime class sometimes differ from its Swift type?
- 4.10.4 How do you create or retarget a class at run time, and why does Swift sometimes ignore it?
← What is key-value coding, what can it do that normal property access… How do you make a computed property observable in KVO, and when do… →