'Use one @StateObject between all view or one per tab-parent view when passing a common view model object - MVVM App
In the following example, I have an app with two Tab Views, Parent View 1 and Parent View 2, and each parent view has child views. All of the views share a view model (cDViewModel) that handles all of the Core Data related stuff. I have read that when passing data around views you should instantiate your object with @StateObject and then pass it around to other child views using @ObservedObject, clear enough. My confusion is that in my case almost all views in the app will be using the cDViewModel. In my app, I'm currently using Option 1 but I for some reason would like to adopt Option 2 if possible.
Does it make a difference which of the two methods below you use when sharing a common object within an MVVM app?
Option 1
This is how I'm currently using it in my app. Please note that I'm declaring the @StateObject inside the TabView section and start sharing it from there, in other words in this scenario only one instance of the cDViewModel is created.
struct MainView: View {
@StateObject var cDViewModel = CoreDataViewModel()
@State var selectedView = 0
var body: some View {
TabView(selection: $selectedView){
ParentView1(cDViewModel: cDViewModel)
.tabItem {
Text("Parent View 1")
}.tag(0)
ParentView2(cDViewModel: cDViewModel)
.tabItem {
Text("Parent View 2")
}.tag(1)
}
}
}
First Tab View
struct ParentView1: View {
@ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: ChildView1(cDViewModel: cDViewModel)){ }
}
struct ChildView1: View {
@ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: OtherChildView(cDViewModel: cDViewModel)){ }
}
// Other Child views...
Second Tab View
struct ParentView2: View {
@ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: ChildView2(cDViewModel: cDViewModel)){ }
}
struct ChildView2: View {
@ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: OtherChildView(cDViewModel: cDViewModel)){ }
}
// Other Child views...
Option 2
Please note that here I'm declaring the @StateObject in each of the parent views and not in the TabView section, for some reason I tend to like this option better but I'm not sure if this could create refreshing issues by having multiple @StateObject declarations.
struct MainView: View {
@State var selectedView = 0
var body: some View {
TabView(selection: $selectedView){
ParentView1()
.tabItem {
Text("Parent View 1")
}.tag(0)
ParentView2()
.tabItem {
Text("Parent View 2")
}.tag(1)
}
}
}
First Tab View
struct ParentView1: View {
@StateObject var cDViewModel = CoreDataViewModel()
NavigationLink(destination: ChildView1(cDViewModel: cDViewModel)){ }
}
struct ChildView1: View {
@ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: OtherChildView(cDViewModel: cDViewModel)){ }
}
// Other Child views...
Second Tab View
struct ParentView2: View {
@StateObject var cDViewModel = CoreDataViewModel()
NavigationLink(destination: ChildView2(cDViewModel: cDViewModel)){ }
}
struct ChildView2: View {
@ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: OtherChildView(cDViewModel: cDViewModel)){ }
}
// Other Child views...
Solution 1:[1]
I think this implementation is a slightly misguided attempt at achieving an MVVM-like architecture (trust me, I've been there).
In MVVM the so-called 'view model' is just as it sounds - a model for each view. It is the object that is responsible for keeping track of state for its view. If a user taps a button, the view model would be notified of the interaction, it does some internal recalculating of state, and then publishes that new state for the view to consume and implement. An example of this might be...
struct MyView: View {
@StateObject private var viewModel = MyViewViewModel()
var body: some View {
VStack(spacing: 20) {
Button("Hello") {
viewModel.helloTapped()
}
if viewModel.helloIsVisible {
Text("Why hello friend.")
}
}
}
}
class MyViewViewModel: ObservableObject {
@Published private(set) var helloIsVisible = false
func helloTapped() {
helloIsVisible = true
}
}
In this example, the view model holds the state of the view (helloIsVisible) and publishes it for the view to consume. When the user interacts with the button, the view model recalculates state.
The flaws I see in your implementation are as follows:
- You seem to be combining the view model with what should be another object in the service layer of your app. Your view and view model should be completely dumb about how the data that drives your view (hopefully in the form of a concise model) were fetched from
CoreData. You can delegate the task of fetching the data and packaging it into a consumable model for the view model into another object (another class) in the service layer of your app. This could be passed around the app in a number of ways, preferably as a dependency, but also perhaps instantiated as a public singleton. It would be wise to read up on the different techniques of making this service available. The view model would either ask this 'CoreData fetching' class for the data that it needs, or the data would be injected as a dependency. Read about singletons here and dependency injection here. - Each view should have its own view model. Presumably the views are different in some way - otherwise, why have more than one? Create a separate view model for each, that each uniquely manage the state of its corresponding view. They should each have a similar mechanism for retrieving data from the 'CoreData fetching' class.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Carter Foughty |
