Why don't willSet/didSet fire for an assignment inside init, and how do you work around it?
The counter below logs on every change through didSet, but the value passed to init is never logged. Explain why the observer is skipped, then fix the class so the effect also runs for the initial value.
Constraints:
- Keep the
didSetobserver for later writes. - Do not change the call sites that construct a
Counter. - The log must fire once for the initializing value too.
final class Counter {
var value: Int = 0 {
didSet { print("value changed to \(value)") }
}
init(start: Int) {
self.value = start // didSet does NOT print here
}
}
Find and fix the bug.
Inside init the object is not fully initialized, so the first assignment installs the value and Swift skips the observers. Work around it by running the effect after init, or by using a property wrapper whose setter always runs.
- ✗Expecting
didSetside effects to run for the value assigned ininit - ✗Blaming the observers being stripped by the presence of a custom initializer
- ✗Reaching for
lazyinstead of running the effect explicitly after init
- →Do observers run when a property is set through a method called from
init? - →Why does a property wrapper's setter run for the initializing assignment?
The observer is skipped because inside init the object is not yet fully initialized: the first assignment to a stored property installs the value rather than changing it, so willSet/didSet are not invoked. The most direct workaround is to factor the effect into a method and call it after the assignment in init:
final class Counter {
var value: Int = 0 {
didSet { log() }
}
init(start: Int) {
self.value = start
log() // run the effect the observer skipped
}
private func log() { print("value changed to \(value)") }
}
The alternative is to encapsulate the rule in a property wrapper: its wrappedValue setter runs even for the initializing assignment, so the effect cannot be skipped and the call sites stay unchanged.