Search code examples
iosswiftswiftuicombine

How to tell SwiftUI views to bind to nested ObservableObjects


I have a SwiftUI view that takes in an EnvironmentObject called appModel. It then reads the value appModel.submodel.count in its body method. I expect this to bind my view to the property count on submodel so that it re-renders when the property updates, but this does not seem to happen.

Is this a bug? And if not, what is the idiomatic way to have views bind to nested properties of environment objects in SwiftUI?

Specifically, my model looks like this...

class Submodel: ObservableObject {
  @Published var count = 0
}

class AppModel: ObservableObject {
  @Published var submodel: Submodel = Submodel()
}

And my view looks like this...

struct ContentView: View {
  @EnvironmentObject var appModel: AppModel

  var body: some View {
    Text("Count: \(appModel.submodel.count)")
      .onTapGesture {
        self.appModel.submodel.count += 1
      }
  }
}

When I run the app and click on the label, the count property does increase but the label does not update.

I can fix this by passing in appModel.submodel as a property to ContentView, but I'd like to avoid doing so if possible.


Solution

  • Nested models does not work yet in SwiftUI, but you could do something like this

    class SubModel: ObservableObject {
        @Published var count = 0
    }
    
    class AppModel: ObservableObject {
        @Published var submodel: SubModel = SubModel()
        
        var anyCancellable: AnyCancellable? = nil
        
        init() {
            anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
                self?.objectWillChange.send()
            }
        } 
    }
    

    Basically your AppModel catches the event from SubModel and send it further to the View.

    Edit:

    If you do not need SubModel to be class, then you could try something like this either:

    struct SubModel{
        var count = 0
    }
    
    class AppModel: ObservableObject {
        @Published var submodel: SubModel = SubModel()
    }