SwiftUI change on multilevel children Published object change

Viewed 604

I have an ObservedObject AppStatus class that has multiple Published classes inside itself. If I have only level in terms of children, everything is working great.

The problem comes when I have a class RecordingTimeManager that has another variable inside (2-levels of children). The variable maxRecordingTime is changing properly when I press the button, it prints '15 fifteenSeconds' but the foregroundColor is not triggering the change. I am not sure if this is a SwiftUI bug or I should structure the relationships in another way:

// AppStatus

// Recording
@Published var recordingTimeManager: RecordingTimeManager = RecordingTimeManager()

// RecordingTimeManager

class RecordingTimeManager {
    @Published var maxRecordingTime: TimeSeletedTime = .sixteenSeconds
...

// SwiftUI component that would need to change the opacity based on a maxRecordingTime change (.foregroundColor is not changing)

Button {
    appStatus.recordingTimeManager.maxRecordingTime = .fifteenSeconds
    print("15 \(appStatus.recordingTimeManager.maxRecordingTime)")
} label: {
    Text("15")
        .font(Font.custom("BwGradual-Bold", size: 15))
        .foregroundColor(appStatus.recordingTimeManager.maxRecordingTime == .fifteenSeconds ? CLAPSOFFWHITE : TRIBESGREY)
}

Many Thanks,

1 Answers

Observable objects don't just work when you have multiple levels of classes. The @Published property wrapper notifies the view when the property has changed, but because this property is a class - a reference type - it doesn't actually change when you change one of its properties. In other words, the reference remains the same.

And the inner @Published doesn't do anything because there's nothing directly observing it (even if RecordingTimeManager conformed to an ObservableObject)

So, you either need to make RecordingTimeManager a value-type - a struct:

struct RecordingTimeManager {
   var maxRecordingTime: TimeSeletedTime = .sixteenSeconds
}

Or, if it must be a class (perhaps because it has some internal state and life cycle), then you could create an inner view that directly observes it.

First, it needs to be an ObservableObject:

class RecordingTimeManager: ObservableObject {
    @Published var maxRecordingTime: TimeSeletedTime = .sixteenSeconds

and then create a view (could be a private internal view) that observes it:

struct MainView: View {
   @StateObject var appStatus: AppStatus = .init()

   private struct InnerView: View {
      @ObservedObject var recordingManager: RecordingTimeManager

      var body: some View {
         Button {
           recordingManager.maxRecordingTime = .fifteenSeconds
         } label: {
           Text("15")
             .font(Font.custom("BwGradual-Bold", size: 15))
             .foregroundColor(
                 recordingManager.maxRecordingTime == .fifteenSeconds 
                    ? CLAPSOFFWHITE : TRIBESGREY)
         }
      }
   }

   var body: some View {

      InnerView(recordingManager: appStatus.recordingTimeManager)
   }
}
Related