How do you make a computed property observable in KVO, and when do notifications fire?
Predict first
A computed full depends on first and last through keyPathsForValuesAffectingFull. What is logged?
final class Model: NSObject {
@objc dynamic var first = "Ada"
@objc dynamic var last = "Lovelace"
@objc dynamic var full: String { "\(first) \(last)" }
@objc class func keyPathsForValuesAffectingFull() -> Set<String> { ["first", "last"] }
}
let m = Model()
nonisolated(unsafe) var log: [String] = []
let t = m.observe(\.full, options: [.old, .new]) { _, c in log.append("\(c.oldValue ?? "-")->\(c.newValue ?? "-")") }
m.first = "Grace"
m.last = "Hopper"
print(log)
t.invalidate()
m.first = "X"
print(log.count)
- two entries then 2
- one entry then 1
- two entries then 3
Mental model. Derived keys are a note in the margin: “the total on page 3 depends on the numbers on pages 1 and 2”. When either of those changes, the announcer also reads out page 3.
A computed property has no setter to intercept, so KVO cannot know when it changes. You declare the dependency instead: override class func keyPathsForValuesAffecting<Name>() -> Set<String> (for full it is keyPathsForValuesAffectingFull) and return the keys it derives from. When first or last changes, KVO also sends the change notifications for full, with old and new values computed by re-reading it. A to-many relation uses mutableArrayValue(forKey:) or the collection accessors so insertions and removals are reported as such.
Notifications are automatic for a dynamic property: one per assignment, even if the value did not change. Return false from automaticallyNotifiesObservers(forKey:) and call willChangeValue/didChangeValue yourself when you need to coalesce several writes into one notification or skip no-op sets. The handler runs synchronously on the thread that made the change, so a background write calls your closure on that background thread, and the handler must be quick and must not mutate the same property recursively.
Each write to a dependency yields a notification for full, and after invalidate() the next write is not delivered.
Common traps
- Getting the method name wrong. The capitalisation of the key is part of the selector, and a typo means no derived notifications and no warning.
- Updating UI from the handler without checking the thread. It runs on the writer’s thread.
They might follow up with
- How do you get the old value? Pass
options: [.old, .new]toobserve. - Does assigning the same value notify? Yes, by default; switch to manual notification to filter it.
Related questions
- 2.4.4 NotificationCenter, delegate, closure or KVO: how do you choose?
- 2.4.2 How do you avoid leaks and crashes with notification observers?
← What does KVO do to an observed object at run time, and how can you…