Child view-models state management with SwiftUI and Combine

This is more of an opinion/architecture question, with questions that arose out of chapter 15 from the Combine + SwiftUI book.

I have a rather larger screen that I am building with SwiftUI and Combine. I am using MVVM with an ObservableObject object acting as the VM. The main view itself is composed of smaller child views, each of which have their own “dummy” view models (that are currently not ObservableObject but rather simply structs) that take in the model and format the data for the component. I have a few questions and I would be curious to hear the community’s thoughts:

1. Where should the child view models be stored?

Currently, I expose the child view models as @Published within my main view model (let’s call it CheckoutViewModel). I then have a Combine publisher inside CheckoutViewModel that does a few things and updates the VMs:

class CheckoutViewModel: ObservableObject, Identifiable {
    
    // MARK: - Publishers
    
    @Published var detailsViewModel: DetailsViewModel?
    @Published var optionsListViewModel: OptionsListViewModel?
    // etc...
    
    // MARK: - Private Members

    private var subscriptions = Set<AnyCancellable>()    
    private let repository: CheckoutRepository // DI'd

    // MARK: - Actions

    func load(productId: String) {
       repository.fetchData(forProductId: productId)
            .sink(
                receiveCompletion: { _ in },
                receiveValue: { product in
                   self.detailsViewModel = DetailsViewModel(fromProduct: product)
                   self.optionsListViewModel = OptionsListViewModel(fromProduct: product)
                }
            )
            .store(in: &subscriptions)
    }
}

Within my CheckoutView, I then call load and have the child VMs passed to the child views:

struct CheckoutView: View {
   @ObservedObject var viewModel: CheckoutViewModel

   var body: some View {
      VStack {
         if let detailsViewModel = viewModel.detailsViewModel {
            ProductDetails(viewModel: detailsViewModel)
         }

         if let optionsListViewModel = viewModel.optionsListViewModel {
            OptionsList(viewModel: optionsListViewModel)
         }
         
         // ... and so on
      }
   }
}

I am not sure if I love this approach. Something feels odd about publishing these child view models like this.

Should each of these child view models be observable objects on their own that then communicate to the parent view model?

As you can see, once you bring in user interactions (i.e. a user can select an option in the OptionsList), my current approach may fall somewhat short or become unmanageable the larger the view gets.

This leads me to my second question.

2. Who should own the state? How should user interactions be facilitated?

As you can imagine, state in this case becomes very important, and with something like a checkout screen, there may be a lot of state to track. This is where I am totally lost currently. With this approach, I know I can store state directly in CheckoutViewModel, but it may just end up becoming too large and cumbersome to work with.

To deal with user interactions, I’ve tried passing down bindings directly to the child views and go from there, but that circumvents the child view models and “clutters” CheckoutViewModel with a bunch of state variables (that seems like they should be part of a model, not the view model).

Another approach was using closures in the child views, i.e. if the user selects a new option from OptionsList, call a closure with the option ID and then call the main view model from the view to update the state - but that seemed super cumbersome and I kept thinking there must be a better way to do it.

Maybe some sort of store or similar would be a good idea? I am trying to get an idea of best practices for how child views/child VMs should communicate with their parent.

Conclusion

As you can probably tell, I am a bit lost about proper state management and how to deal with child views using MVVM in SwiftUI. Any guidance or general ideas/suggestions would be greatly appreciated. Is using MVVM in SwiftUI maybe a bad idea in general? As anyone else done something similar, or am I on the completely wrong path here?

I think that’s a really, really good question as it highlights some of the current shortcomings of SwiftUI — difficult to compose data modeling which stems in the lack of reasonable way to navigate or route the users inside an app. There are new iOS 16 APIs that take care of navigation but data modeling is still an open issue.

What I’d do is have a single data model for any reasonable amount of views and than pass that around as an environment object so all nested views can access it and pass it over their controllers if needed. I think injected data models into the environment is one of the really clever aspects of SwiftUI because you can easily test parts of your view hierarchy and inject test mocks in the environment instead of the real models. In any case I can’t say I do have a one-fits-them-all solution for this, sadly.

If you want to see more advanced approach to data modelling for SwiftUI, have a look at the composable architecture here: GitHub - pointfreeco/swift-composable-architecture: A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. they do something very clever to compose and de-compose data models throughout the view hierarchy.