MVVM: A UI design pattern
Dec 29, 2021
Introduction :
MVVM is a UI architectural pattern, and it stands for Model-View-ViewModel. MVVM is an MVC variant created by Microsoft, that aims to minimize the boilerplate of syncing View events and Model updates that Controllers hold.
With the help of a ViewModel component, Microsoft's MVVM solution seamlessly connects View events with Model updates.
Developers declare the ViewModel bindings along with the View declarations in XAML, and the framework handles the runtime bindings automatically, according to Microsoft's.NET graphics system.
Because most platforms don't offer the automated wiring that the .NET graphics system offers, MVVM is often known as Model-View-Binder outside of Microsoft systems.
In UIKit, for example, there is no mechanism to automatically connect a ViewModel to a View, therefore the developer must implement the Binder components. It's usual to use frameworks like RxSwift or Combine (or closures !) to make the connections between the View and the ViewModel easier.
Comparing MVC and MVVM :
The only difference between the MVC and MVVM structures is that a ViewModel does not hold a reference to the View as the Controller does in MVC.
To make both the View and the ViewModel more reusable, they should not be directly dependent on one other. Through a binding mechanism, they should be able to communicate in an indirect manner.
UIViewControllers are ideal candidates for acting as Binders in UIKit, linking the View to the ViewModel. This is because in order to show views on the screen in UIKit, your app must have at least one UIViewController.
Since UIViewControllers already have a reference to their corresponding Views, they’re a great place to inject and bind the ViewModel with the View.
But, in UIKit, MVVM won't reduce a lot of boilerplate code since there’s no automatic binding. Still, ViewModels are an good idea to create a reusable logic between UI and core components and a better architectural separation. It’s recommended for ViewModels to be platform and framework-agnostic so that we can reuse them on multiple platforms.
MVVM in practice :
Imagine a simple ViewController :
We can see that the ViewController is communicating with the ModelLoader, it also manage the model loading state with the var onRefresh and it create and configure a UIRefreshControl.
The goal is to move some responsibilities out of the ViewController to a ViewModel.
There is two common ways to create a ViewModel, stateful and stateless.
Stateful version:
So the ViewModel can be on multiple kind of state (loading, pending, etc...) and we notify the observer any change on the state with a simple closure (onChange).
We also define the state transitions by setting the var state in the loadModel() method. Since the state is private, we expose accessors for the current state of the ViewModel with isLoading (a switch statement is enough in this case) and a computed var to access the model, if it's loaded, we return the model otherwise we return nil.
That's all we need in the ViewModel, but what about the ViewController ?
First we replace the ModelLoader reference with a viewModel property.
Secondly in the refresh function we set the onChange callback to get a viewModel and its isLoading property to determine if we need to refresh or not.
Thirdly, we need to see if we have a model in the ViewModel, and pass it with the onRefresh closure.
And finally, we tell the ViewModel to load the model
So the onChange closure is the binding logic between our the ViewModel and the View, happening in the ViewController. We can even wrap the closure in a binded function, so the View is a binded UIRefreshControl:
Notice the last lines of code, we don't set the viewModel as a target action with the view. That's because the target action pattern force the instance to be an NSObject, and conforming to NSObject is a UIKit requirement. View models should be platform agnostic, by making the ViewModel subclass of NSObject, we leak implementation detail just to satisfy UIKit.
By keeping self, it's the ViewController that forward the message to the ViewModel.
There is another code smell, the onRefresh reference the Model ! We can move this logic to the ViewModel, because the ViewController is only passing the model, it does nothing with it.
Now our ViewController does not manage any state, it binds the View to the ViewModel. All the state management now live in a stateful version of the ViewModel.
We don't need to hold the model state, by just forwarding it to the onRefresh closure, we can get rid of
self?.state = .loaded(model)
We still need to update the View, so the new state is .pending. So we end up with two state, either loading or pending.
And because we have only two state, a simple boolean isLoading is enough, we can remove the State enum and the model computed var.
Let's transform the ViewModel to a stateless version.
Stateless version:
We can see that the ViewModel hold state with the `isLoading` boolean, to get a transient state we can use an closure observer for each state.
By having a onLoadingStateChange we can pass the state transition in the closure. And in the ViewController we have this now :
In the binded function we set the onLoadingStateChange pass a boolean, and weakify view, since we do not need a reference to self.
Bonus : Generics and typealias
In the ViewModel, we can use Generics and typealias, that way we do not leave room for assumptions.