I find calling a mutating method on a value wrapped by a property wrapper always causes its didSet called, even if the mutating function doesn't actually write the value. This behavior is different if the value isn't wrapped.
import Foundation
import Combine
let defaultValue = 1
struct Foo {
var x: Int = defaultValue {
didSet {
print("[Regular] Changed! oldValue: \(oldValue), newValue: \(x)")
}
}
mutating func test(_ value: Int) {
if x != value {
x = value
}
}
}
// test 1: a baseline test. The value isn't wrapped. DidSet isn't called (as expected).
var foo = Foo()
foo.test(defaultValue)
@propertyWrapper
struct SampleWrapper1 {
private var foo: Foo
init(wrappedValue: Foo) {
self.foo = wrappedValue
}
var wrappedValue: Foo {
get { return foo }
set { foo = newValue }
}
}
struct Baz {
@SampleWrapper1 var foo = Foo() {
didSet {
print("[Baz] Changed! oldValue: \(oldValue), newValue: \(foo)")
}
}
}
// test 2: the value is wrapped by a dummy property wrapper. Calling mutating method always causes its didSet called, even if the method doesn't write the value.
var baz = Baz()
baz.foo.test(defaultValue)
// Output: [Baz] Changed! oldValue: Foo(x: 1), newValue: Foo(x: 1)
class Bar: ObservableObject {
@Published var foo = Foo() {
didSet {
print("[Published] Changed! oldValue: \(oldValue.x), newValue: \(foo.x)")
}
}
}
// test 3: this is similar to test 2, except that it uses Publisher property wrapper.
var bar = Bar()
Just(defaultValue).sink { value in
bar.foo.test(value)
}
// output: [Published] Changed! oldValue: 1, newValue: 1
Test1 output nothing, which is what I expected. Test2 and test3, however, shows that didSet gets called even if the wrapped value isn't overwritten. I wonder if this is by design or is it a bug?
I noticed this behavior in a scenario like test3. I didn't expect the publisher emitted value, but it emitted. It took me a while to identify the root cause.
When you call a mutating method on a member of a struct (after the struct is initialized), then that member is mutated and didSet is called. This is a fundamental thing to understand about mutating.
By calling test, you are mutating baz.foo (and, in my example, boo.foo), but you're not mutating baz.foo.x (nor, in my example, boo.foo.x).
By contrast, you can manipulate boo.foo.x directly, setting both boo.foo and boo.foo.x:
Thanks, what you said all make sense to me. One thing that confused me was that the Foo.test() didn't actually write to the storage but didSet still got called. Based on what you said and the experiments, that's the expected behavior. For example, if I change Foo.test() to an empty function, didSet will still be called in your test.
So my test 1 is a red herring. When Foo.test() get called, it's the Foo value is change and its didSet got called (as shown in your test). Whether Foo.x's didSet got called depends on the code in Foo.test().
BTW, I just checked the Swift Programming Guide. I think the following text in it might be a bit inaccurate:
You have the option to define either or both of these observers on a property:
willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.
The exact meaning of "value is stored" in the text is not clear. Based on the above discussion I guess it should mean "the call of setter", but it's very easy for people to misunderstand it as "actually saving the value to storage".
EDIT: Or maybe an empty mutating function should be considered as writing a new value? That's very likely the right way to think about it :)