How to handle state with nested Observable objects

I'm trying to understand the best approach for handling state with you have an object that contains other observable objects.

For instance, say your main model has an array of other observable objects. Is this the correct approach to have everything update when changes are made in a subview? I feel like this should be done through bindings, but I can't get it to work like that.

class Message: Identifiable, ObservableObject {
    var id = UUID()
    @Published var text: String
    
    init(text: String) {
        self.text = text
    }
}

class Messenger: ObservableObject {
    @Published var messages: [Message] = []
}

struct MessageView: View {
    @StateObject var message: Message
    var body: some View {
        Button {
            message.text = "New Message!"
        } label: {
            Text(message.text)
        }
    }
}

struct ContentView: View {
    @StateObject var messenger = Messenger()
    var body: some View {
        VStack {
            
            Button {
                messenger.messages += [Message(text: "Hello!")]
            } label: {
                Text("Send Message")
                    .font(.headline)
            }
        
            ForEach(messenger.messages) { message in
                MessageView(message: message)
            }
        }
    }
}
1 Like

This is an important question. Refactoring a view model can quickly break the expected chains of update propagation ...

Here are a few thoughts:

  • A basic idea of SwiftUI (and good design in general) is that you provide each view exactly what it needs to display – not more. Basically every view is bound to its corresponding (View-) Model. In that principle scenario, there is no need to propagate updates from elements of a view model to higher levels of that view model.
  • It seems you already implemented that principle: Messenger does not need to observe its messages. Instead, each MessageView observes its message.
  • Be aware that ContentView itself will not update when one of its messages in messenger changes. Messenger only fires when its messages array changes. Since Message is a class, the array only holds pointers to messages. But the pointers themselves don't change when the messages change. It would be a different story if Message were a struct.
  • In your example, ContentView creates and owns the messages, so you want MessageView to hold its Message as an @ObservedObject rather than a @StateObject.