Rod_Brown
(Rod Brown)
1
Hi all,
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.
Thanks,
Rod
1 Like
This might be SR-12178, I have a fix for it here. If not, then it might be a different bug.
dlbuckley
(Dale Buckley)
3
You aren't the first one to notice this. There is a bug report for the exact issue here: [SR-12089] Swift 5.2 snapshot: didSet not called · Issue #54525 · apple/swift · GitHub and another report about it on the forums here: Property wrapper observer not firing
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.
2 Likes
Rod_Brown
(Rod Brown)
5
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.
Panajev
(Goffredo Marocchi)
6
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.
3 Likes
beccadax
(Becca Royal-Gordon)
7
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.
1 Like
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"
3 Likes
MasasaM
(Masashi Aso)
9
Here's a minimum case.
This bug will occur with mutating operations.
@propertyWrapper
struct A<V> {
var wrappedValue: V
}
class C {
@A var x = 0 {
didSet { print(x) }
}
}
extension Int {
mutating func f() {
self = 1
}
}
C().x.f() // doesn't print
C().x += 2 // doesn't print
C().x = 3 // prints '3'
3 Likes
DnV1eX
(Alexey Demin)
10
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!
2 Likes
This is clearly an example of _modify being used now instead of get and set. That it's the reason why += does not work but value = value + 1 does.
This is now fixed on master and I have opened a PR to pull the fix into 5.2 as well.
7 Likes
I can confirm the fix has made its way to Xcode 11.5 beta.
9 Likes
gui_dos
(Guido Soranzio)
14
I had to use a logical negation instead of toggle() if I wanted to store the new boolean value of a SwiftUI @Published property in UserDefaults.
With the new Swift 5.2.4 in Xcode 11.5 beta I confirm you can use toggle() again.
class Settings: ObservableObject {
@Published var mutedAudio: Bool = UserDefaults.standard.bool(forKey: "mutedAudio") {
didSet { UserDefaults.standard.set(self.mutedAudio, forKey: "mutedAudio") }
}
}
Button(action: {
self.settings.mutedAudio.toggle()
// self.settings.mutedAudio = !self.settings.mutedAudio // workaround in iOS 13.4
}) {
Image(systemName: settings.mutedAudio ? "speaker.slash.fill" : "speaker.2.fill").resizable().frame(width: 24, height: 24)
}
royhsu
(Roy Hsu)
15
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
}
}
}
1 Like
Rod_Brown
(Rod Brown)
16
Hi @surashsrijan,
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?
1 Like
Could you post a full reproducer?
Rod_Brown
(Rod Brown)
18
Copy and paste this into a playground:
import Combine
class MyClass {
@Published var publishedInt = 5 {
didSet { print("Published int updated") }
}
var unpublishedInt = 5 {
didSet { print("Unpublished int updated") }
}
init(publishedInt: Int, unpublishedInt: Int) {
self.publishedInt = publishedInt
self.unpublishedInt = unpublishedInt
}
}
let example = MyClass(publishedInt: 4, unpublishedInt: 4)
When run, the published print is performed, but the unpublished print is not.
1 Like
jonprescott
(Jonathan Prescott)
19
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)
1 Like
pyrtsa
(Pyry Jahkola)
20
This pitch seems closely related and AFAICT explains your observed current behaviour: Allow Property Wrappers with Multiple Arguments to Defer Initialization when wrappedValue is not Specified
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.