The proposal for property wrappers offers only this for explaining how they are intended to interact with willSet
and didSet
:
Those properties can have observing accessors (
willSet
/didSet
), but not explicitly-written getters or setters.
But since property wrappers and meant to blur the line between a public facing stored property and an underlying, hidden wrapper type, things get very murky with willSet
/didSet
.
Consider the following very simple property wrapper:
@propertyWrapper
struct Wrap<Value> {
var wrappedValue: Value
init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
If one were to use it with didSet
like so:
@Wrap var value = 0 {
didSet { print("didSet") }
}
…then the didSet
is called as expected. You can see this with the following test:
import Testing
@Test func accessors() async throws {
@Wrap var count = 0 {
didSet { Issue.record() }
}
withKnownIssue {
count = 1 // ✅ Passes
}
}
That proves that the didSet
is called when mutating the count
variable directly.
But there are other ways to mutate count
. You can also go through _count.wrappedValue
, and doing so does not trigger didSet
:
withKnownIssue { // ❌ Fails
_count.wrappedValue = 1
}
It seems reasonable to me that any mutation of wrappedValue
would trigger the observers on count
.
This discrepancy in behavior becomes more problematic when dealing with projected values that may have mutating functions:
$count.compute()
If that compute
method mutates the wrappedValue
it will not trigger didSet
.
This problem also affects SwiftUI, in which bindings to @State
do not trigger didSet
:
struct CounterView: View {
@State var count = 0 {
didSet { print("This will not be called") }
}
var body: some View {
Form {
Stepper("\(count)", value: $count)
}
}
}
Any change made to count
via the binding will not trigger didSet
.
Is any of this behavior documented somewhere? There is no mention of this in the proposal. And is any of the behavior surprising or possibly considered a bug? It is a source of a lot of confusion to want to tap into the didSet
of a property wrapper field and find that only under certain circumstances will it actually trigger.