23

I have just updated to XCode 11.4 and some of my code has stopped working. I have some @Published struct variables in an ObservableObject. Previously, when I updated properties on the struct, the didSet method would fire on the published property, but that's not the case anymore. Is it possible that this behaviour has changed by design in the latest update to Swift?

Here's a trivial example:


import SwiftUI

struct PaddingRect {
  var left: CGFloat = 20
  var right: CGFloat = 20
}

final class SomeStore : ObservableObject {
  @Published var someOtherValue: String = "Waiting for didSet"

  @Published var paddingRect:PaddingRect = PaddingRect() {
    didSet {
      someOtherValue = "didSet fired"
    }
  }
}

struct ObserverIssue: View {
  @ObservedObject var store = SomeStore()

  var body: some View {
    VStack {
      Spacer()

      Rectangle()
        .fill(Color.yellow)
        .padding(.leading, store.paddingRect.left)
        .padding(.trailing, store.paddingRect.right)
        .frame(height: 100)

      Text(store.someOtherValue)

      HStack {
        Button(action: {
          // This doesn't call didSet
          self.store.paddingRect.left += 20

          // This does call didSet, ie. setting the whole thing
//          self.store.paddingRect = PaddingRect(
//            left: self.store.paddingRect.left + 20,
//            right: self.store.paddingRect.right
//          )

        }) {
          Text("Padding left +20")
        }

        Button(action: {
          self.store.paddingRect.right += 20
        }) {
          Text("Padding right +20")
        }
      }

      Spacer()
    }
  }
}

struct ObserverIssue_Previews: PreviewProvider {
    static var previews: some View {
        ObserverIssue()
    }
}

The property updates, but didSet does not fire.

Is it possible to get nested properties of a struct to trigger the didSet method of the publisher?

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
codewithfeeling
  • 6,236
  • 6
  • 41
  • 53
  • Strange that it was working in my code until I upgraded, that's all. I will change the title of the question as you see my actual question is "Is it possible to get nested properties of a struct to trigger the didSet method of the publisher" – codewithfeeling Mar 30 '20 at 15:01
  • 1
    @Asperi: i had the same issues....and didSet was called before my update - that's true. maybe it was an error before...but it worked ;) and i liked the functionality and i am missing it.... – Chris Mar 30 '20 at 15:21
  • check this out : https://stackoverflow.com/a/59391476/8457280. -> then it works again ;) – Chris Mar 30 '20 at 15:39
  • I cannot get this to work even for unpublished vars, so maybe the problem is in the ObservableObject – Cristi Băluță Jul 22 '22 at 06:31

2 Answers2

38

You can subscribe to the @Published value stream in the class itself.

final class SomeStore: ObservableObject {
    @Published var someOtherValue: String = "Waiting for didSet"
    @Published var paddingRect: PaddingRect = PaddingRect()
    private var subscribers: Set<AnyCancellable> = []
    
    init() {
        $paddingRect.sink { paddingRect in
            print(paddingRect) // 
        }.store(in: &subscribers)
    }
}

Note that the sink closure will be called on willSet, though.

Geri Borbás
  • 15,810
  • 18
  • 109
  • 172
  • 19
    Bear in mind `Published.Publisher` (which is accessed via `$paddingRect`) is implements _willSet_, not _didSet_. – Dávid Pásztor Dec 10 '20 at 10:56
  • @DávidPásztor Yep, this difference in expectations can lead to nasty subtle bugs: https://forums.swift.org/t/is-this-a-bug-in-published/31292 – Tomáš Kafka Aug 21 '21 at 21:27
12

The property observer observes the property. The trouble goes from new Swift syntax related to property wrappers. In your case you try to observe if value of Published (which is a struct defining the specialized property wrapper) did change, not the value of the wrapped property.

If you need to monitor left or right values in PaddingRect, simply observe this values directly.

import SwiftUI


struct PaddingRect {
    var left: CGFloat = 20 {
        didSet {
            print("left padding change from:", oldValue, "to:", left)
        }
    }
    var right: CGFloat = 20 {
        didSet {
            print("right padding change from:", oldValue, "to:", right)
        }
    }
}

final class SomeStore : ObservableObject {
    @Published var someOtherValue: String = "Waiting for didSet"
    @Published var paddingRect:PaddingRect = PaddingRect()
}

struct ContentView: View {
    @ObservedObject var store = SomeStore()

    var body: some View {
        VStack {
            Spacer()

            Rectangle()
                .fill(Color.yellow)
                .padding(.leading, store.paddingRect.left)
                .padding(.trailing, store.paddingRect.right)
                .frame(height: 100)

            Text(store.someOtherValue)

            HStack {
                Button(action: {
                    // This doesn't call didSet
                    self.store.paddingRect.left += 20

                    // This does call didSet, ie. setting the whole thing
                    self.store.paddingRect = PaddingRect(
                        left: self.store.paddingRect.left + 20,
                        right: self.store.paddingRect.right
                    )

                }) {
                    Text("Padding left +20")
                }

                Button(action: {
                    self.store.paddingRect.right += 20
                }) {
                    Text("Padding right +20")
                }
            }

            Spacer()
        }
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

enter image description here

Or take the advantage that Published projected value is Publisher and aply next modifier to any View

.onReceive(store.$paddingRect) { (p) in
            print(p)
        }
user3441734
  • 16,722
  • 2
  • 40
  • 59