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?