i'm not entirely sure this is precisely what's going on, but i think this is some sort of emergent confusing behavior from the interaction of various language features (and possibly under-specification of how things are supposed to work).
IIUC observing accessors (willSet/didSet) of instance properties will not be run until after the object is fully initialized. with 'regular' stored properties, it seems you actually can't trigger them from init without some indirection, even if self is known to be definitely initialized. you either need to use self from a non-initializer context, or alias the fully-initialized value and perform a mutation through that to get them to fire:
class X {
var p: Int {
didSet { print("set p") }
}
init() {
self.p = 0 // not yet initialized here; `didSet` not called
self.p = 1 // initialized, but observer not triggered... not sure exactly why
let me = self // if we alias the fully initialized instance...
me.p = 2 // then mutations trigger the property observers
// prints: "set p"
}
}
additionally, if you declare a mutable stored property of type T? i think it implicitly 'counts' as being initialized (to nil), so does not require being explicitly set in an initializer. i believe this is a special-cased behavior of mutable, optional properties that use the shorthand optional syntax specifically:
class One {
let i: Int?
init() {} // 🛑 Return from initializer without initializing all stored properties
}
class Two {
var i: Optional<Int>
init() {} // 🛑 Return from initializer without initializing all stored properties
}
class Three {
var i: Int?
init() {} // ✅
}
class Four {
var i: Int? {
didSet { print("set i") }
}
init() {
let me = self
me.i = 1 // prints: "set i"
}
}
setting aside property wrappers, and using an approximate 'desugaring' of what they turn into, then the combination of these two things i think explains why you get the behavior you reported:
class A {
var a: Int {
didSet {
print("didSet a", a)
}
}
var _b: Int? {
didSet {
print("didSet _b \(_b, default: "nil")")
}
}
var b: Int? {
get { _b }
set { _b = newValue }
// for some reason you can't add property observers for 'manually'
// wrapped properties, but you can with property wrappers...
// some discussion here:
// https://forums.swift.org/t/unexpected-behavior-with-property-wrappers-and-willset-didset/76887
}
init() {
self.a = 0
// at this point A is fully initialized as _b is `nil` by default
self.b = 1 // calling this indirects through `b` and triggers the `_b` property observer
// prints: "didSet _b 1"
}
}
so... not sure if that's a particularly satisfying explanation because i agree it's still pretty confusing, but maybe it's better than nothing?