Something I noticed in Swift 5.2 in the latest betas, and the latest snapshots, when you use a property wrapper for a struct, and you set a value inside the struct, a didSet does not get called. For example:
import Combine
struct MyStruct {
var details: String
}
class MyClass {
@Published var myStruct = MyStruct(details: "Test") {
didSet {
print("My struct was set")
}
}
}
let myClass = MyClass()
myClass.myStruct.details = "test" // This should print "My struct was set", but doesn't.
myClass.myStruct = MyStruct(details: "test") // This continues to print "My struct was set".
Is this change in behaviour expected? Seems extremely weird to me, and is causing a few breaking defects inside our codebase.
Unfortunately it doesn't look like its been picked up yet and the closer to the 5.2 release we get the less chance it will get fixed. I'm tagging in @beccadax as he was the one who created the internal Apple tracking ticket.
That looks like the one I mentioned before - I have linked it to my fix! And yeah, it might not make 11.4 but might be available in a point release (like 11.4.1) depending on when my PR is merged.
This is a pretty glaring bug that will blindside anyone who’s relying on these semantics. The only way I noticed is via unit tests failing. I sure hope this makes it into 11.4.
How can we get an official Apple answer about this being addressed for Xcode 11.4? It seems like a regression in observability semantics warrants delaying Xcode 11.4 until it is fixed.
Don't read too much into that—I create nearly all of the radars.
Yes, looking at SIL dumps, I think the modify accessor is the problem in all of these bugs (and I thought so before I looked at your PR).
I don't know when this bug will be fixed (i.e. this is not an "official Apple answer"), but for the @Published examples at least, there's a straightforward workaround: subscribe to the property's projected value (the matching $ property), which is a Publisher that publishes changes to the property, and tie your didSet logic to that.
Also experiencing this issue when using += with a property wrapped int.
Using the released version of Xcode 11.4
This bug completely broke a piece of our app.
Switching it to value = value + 1 works but doesn't seem ideal.
Example code:
import Combine
class MyClass {
@Published var value = 4 {
didSet {
print("Value was set to \(value)")
}
}
}
let myClass = MyClass()
myClass.value = 5 // This prints "Value was set to 5"
myClass.value += 1 // This should print "Value was set to 6", but doesn't.
myClass.value = myClass.value + 1 // This prints "Value was set to 7"
Same here! I cannot believe they knew about the bug months ago and released Swift 5.2 without the fix. It should be considered not a regular medium-priority bug, but critical functionality regression!
I'm not sure if @State was treated differently from @Published property wrapper.
I tested the following code both with @State and @Published in Xcode 11.5 beta (11N605c), but only observers (willSet, didSet) for @Published were triggered.
And I'm also not sure whether this is related to SwiftUI implementation detail or the same bug related to this post?
Maybe I missed something obvious.
struct ContentView: View {
@State
private var isOn = false {
willSet {
print("@State isOn willSet:", isOn) // not working
}
didSet {
print("@State isOn didSet:", isOn) // not working
}
}
@ObservedObject
private var store = ContentStore()
var body: some View {
Form {
Toggle(isOn: $isOn) {
Text("The toggle for @State isOn")
}
Toggle(isOn: $store.isOn) {
Text("The toggle for @Published isOn")
}
}
}
}
final class ContentStore: ObservableObject {
@Published
var isOn = false {
willSet {
print("@Published isOn willSet:", isOn) // working
}
didSet {
print("@Published isOn didSet:", isOn) // working
}
}
}
I'm just curious did that fix change the semantics of the didSet operator to fire even before initialisation has finished?
Something I noticed:
class MyClass: MySuperClass {
private var itemHasChanged = false
@Published private var myPublishedItem: Item? {
didSet {
itemHasChanged = true // This shouldn't fire in the initialiser, but does?
}
}
init(item: Item?) {
self.item = item
super.init()
}
}
If the value is marked as @Published, this fires in the initialiser. If it isn't marked with a property wrapper, it doesn't. Is this expected?
It seems that for @Published variables, the default initialization counts as a setting (where you initialize it to '5'), thus when you change the value in the init method, the value changes and you invoke the didSet. The second unpublishedInt follows the standard Swift convention where init initializes the variable, no matter if it was previously initialized in a initial value statement. I guess the question is, "does a change in a variable's value in an init method count as a change for @Published or not?" Note that if you remove the initialization of publishedInt, no printout results.
The following shows consistent behavior between the two, but allows for default initialization to 5:
import Combine
class MyClass {
@Published var publishedInt : Int {
didSet { print("Published int updated") }
}
var unpublishedInt : Int {
didSet { print("Unpublished int updated") }
}
init(publishedInt: Int=5, unpublishedInt: Int=5) {
self.publishedInt = publishedInt
self.unpublishedInt = unpublishedInt
}
}
let example = MyClass(publishedInt: 4, unpublishedInt: 4)
Namely, wrapped properties get immediately initialised at their declaration if a default value is provided there; assigning them in the initialiser then counts as reassignment and that's why didSet gets (IMO somewhat unexpectedly) called.
If the pitched proposal were to be adopted then neither didSet would get called in your example.