In what order do willSet and didSet run across a subclass, an initialiser, a didSet that assigns, and an inout argument?
Predict first
What does this print?
class Base {
var v = 0 {
willSet { print("Base willSet", newValue) }
didSet { print("Base didSet", oldValue, "->", v) }
}
init() { v = 1; print("Base init done") }
}
class Sub: Base {
override var v: Int {
willSet { print("Sub willSet", newValue) }
didSet { print("Sub didSet", oldValue, "->", v) }
}
override init() { super.init(); print("Sub init, now set"); v = 2 }
}
_ = Sub()
print("---")
struct Counter {
var n = 0 { didSet { print("didSet", oldValue, n); if n > 2 { n = 0 } } }
var items = [Int]() { didSet { print("items changed", items.count) } }
mutating func bump() { n += 1 }
}
var c = Counter()
c.bump(); c.bump(); c.bump()
print(c.n)
c.items.append(1)
c.items[0] = 5
func bump(_ x: inout Int) { x += 1; x += 1 }
bump(&c.n)
print("end", c.n)
- Base init done / Sub init, now set / Sub willSet 2 / Base willSet 2 / Base didSet 1 -> 2 / Sub didSet 1 -> 2 / then three didSets, 0, items changed twice, didSet 0 2, end 2
- Base willSet 1 / Base didSet 0 -> 1 / Base init done / Sub init, now set / Base willSet 2 / Sub willSet 2 / Sub didSet 1 -> 2 / Base didSet 1 -> 2 / then ...
- Base init done / Sub init, now set / Sub willSet 2 / Sub didSet 1 -> 2 / then a recursive didSet
Mental model. A doorbell that rings when someone else comes in, not when you let yourself in with your own key.
Observers do not run for assignments in the owning class's own initialiser: v = 1 in Base.init prints nothing, even though Sub overrides the observers. After super.init(), v = 2 in Sub.init does fire. The observed order on Swift 6.4 is Sub willSet, Base willSet, the store, Base didSet, Sub didSet (a subclass wraps the superclass's observers; I did not find it spelled out in the language guide, so treat it as observed).
Assigning to the property inside its own didSet does not re-trigger the observer: n reaches 3 and is reset to 0 without recursion. Mutating a collection property in place (items[0] = 5, append) counts as a set. Passing a property as inout is copy-in copy-out: the function adds 1 twice, but didSet runs once after the call, with oldValue 0 and the new value 2.
Own-init assignments skip observers; subclass observers wrap the superclass ones; inout fires once.
Common traps
- Relying on observers during init. They do not fire.
- Expecting one call per mutation through inout. You get one per call.
They might follow up with
- Does a didSet that never reads oldValue cost less? It can avoid fetching the old value.
Related questions
- 1.6.1 Stored vs computed vs lazy vs property observers (willSet/didSet)?
- 1.6.3 What is a property wrapper? Can you write a custom one?
← In what order does switch try its cases, and what do where… In what order are operands, default arguments, autoclosures and… →