How does KVO work in Swift, and why does it sometimes never fire?

2.4.5 · L2 · Interview

Predict first

What does this print?

class Model: NSObject { @objc var n = 0 }
let m = Model()
let obs = m.observe(\.n) { _, _ in print("fired") }
m.n = 1
print("done")
_ = obs
  • fired, done
  • done

Predict first

What does this print after adding dynamic?

class Model: NSObject { @objc dynamic var n = 0 }
let m = Model()
let obs = m.observe(\.n) { _, _ in print("fired") }
m.n = 1
print("done")
_ = obs
  • fired, done
  • done

Mental model. KVO is a tripwire the runtime strings across a property. Without dynamic the setter steps over the wire.

Key-value observing lets you watch a property of an object: object.observe(\.name, options: [.new, .old]) { object, change in … }. It returns an NSKeyValueObservation that you must keep alive; it stops when released. KVO is an Objective-C runtime feature, so it only works on subclasses of NSObject, and the property must be @objc dynamic.

The dynamic keyword is the catch. Without it Swift calls the setter directly and never goes through the runtime machinery that posts the change, so the observer compiles but never fires. The callback runs on whatever thread changed the value. Apple frameworks still expose a lot through KVO (AVPlayerItem.status, URLSessionTask.progress), while your own state is better served by @Observable, @Published or didSet.

Without dynamic the setter bypasses the runtime machinery, so no change is ever reported.

dynamic routes the setter through the observation machinery, so the handler runs inline during the assignment.

Common traps

  • Forgetting dynamic. No crash and no callback, which is the worst kind of bug.
  • Not storing the observation. It is released immediately and nothing is ever delivered.

They might follow up with

  • What was the old crash? The legacy addObserver(_:forKeyPath:) API threw if an observed object deallocated, or you removed an observer you never added.
  • What does .initial do? It delivers the current value immediately, as the first callback.

Related questions

  • 2.4.4 NotificationCenter, delegate, closure or KVO: how do you choose?
  • 1.6.1 Stored vs computed vs lazy vs property observers (willSet/didSet)?

← NotificationCenter, delegate, closure or KVO: how do you choose?   What do Operation and OperationQueue give you that GCD does not, and… →