Let me explain by giving an example:
Let's say we are building an app for managing movies. If we use MVVM with SwiftUI and implement 3 screens then we will end up with
3 view models.
MovieListScreen -> MovieListViewModel
AddMovieScreen -> AddMovieViewModel
MovieDetailScreen -> MovieDetailViewModel
Each VM will require access to a HTTPClient to perform the network call etc. This can be injected through the use of dependency injection. If you have any view related logic
then you will put that in the VM.
This is a typical use case for MVVM with SwiftUI application.
Whenever we add a new VM we make sure that it is conforming to ObservedObject. This means we are creating a new source of truth. But why? The source of truth has not changed. It is still the server.
Another problem is that whatever the VM is doing, View can also do because View has all the binding features like @State, @Binding, @Environment and @EnvironmentObject. Most of the time you can test view presentation logic just by using Xcode previews. Other times you can extract the presentation logic into a simple struct and unit test that struct.
So, in other words we created new source of truths (view models) for no good reason.
So, how can we fix this.
For small client/server app we create a single source of truth (For larger apps you can create more based on the bounded context of the application and NOT based on the new screen we added).
Let's call it MovieStore (instead of MovieModel). MovieStore will manage the entire state of the application. MovieStore
can be injected into the EnvironmentObject also so it is available to all the views. Why can't we call it MovieViewModel instead of MovieStore? The main reason you should not call it MovieViewModel
is because MovieViewModel name usually represents a VM for a particular screen and does not represent the entire state of the application. Typically when you create a view model it is used on a particular screen.
So, let's get back to the same example code:
MovieListScreen -> MovieListViewModel (deleted)
AddMovieScreen -> AddMovieViewModel (deleted)
MovieDetailScreen -> MovieDetailViewModel (deleted)
We will delete all the view models and replace all of them with a single store called "MovieStore". So now we have
MovieListScreen -> MovieStore
AddMovieScreen -> MovieStore
MovieDetailScreen -> MovieStore
You can access MovieStore inside the screens using EnvironmentObject. This makes our code much easier to maintain and it takes advantage of all the SwiftUI features provided by Apple.
* I use the above pattern for client/server apps. If I am building a Core Data/Swift Data app then I will use a variation of Active Record Pattern. For apps that use both client/server
and CoreData/SwiftData then you can merge them both based on their usage.