MVVM
The MVVM (Model-View-ViewModel) architecture is a way of organizing — or more generally, a design paradigm — an application's code. MVVM starts with the premise that applications consist of two core parts:
The View is where all of our user-interface (i.e., frontend) code lives. The Model is where all of our logic (i.e., backend) code lives. The ViewModel serves as a liason betwen the View and the model, binding the View to the Model.
The Model
The Model is completely UI-independent. Under MVVM, no UI code is ever
placed in the model. For example, we would never import SwiftUI
into a
model source code file.
The only code we should see in the model is code that relates to:
- data, or
- logic
One way to think about this: If another developer wanted to know what our
application does, they would first look at the source code files inside our
Model
directory.
For example, consider a Chess game app. The Model
would contain code
about the following:
- Verifying that move is valid
- Calculating how many points have accumulated
- Keeping track of which pieces are still on the board
- Storing the amount of time elapsed
Importantly, the model is the single source of truth for our application's data. No other part of the application, outside the model, keeps data. This is a strict principle under MVVM. Other parts of the application are not even permitted to store that data inside a local variable.
By ensuring that there's one, and only one, truth source, we don't have to worry about two different versions of the truth. This prevents situations like our Chess game's UI showing that the time elapsed is an hour while our model says it's only been five minutes.
The View
The View is where all of our UI code is placed. If another developer wanted
to know how our application looks the way it does — e.g., buttons with
shadows, a list view, colors, etc. — they would look inside the View
directory.
Using our chess game app again, the View
would contain code about:
- what a particular piece looks like
- the board's alternating square colors
- what the point board looks like
As mentioned earlier, the View cannot contain data. That's the Model's prerogative. But wait. Isn't the View in charge of showing data like the scoreboard or time? Yes, it is. For the View to render the current score or time, it needs that data. And to get that data, it must ask for it.
Because the View never holds data, it is always a current state reflection of the model. At no point does it show data other than what the model currently holds.
In other words, the data the View receives is read-only. This means that
the View has no state. If there ever is a state (indicated by the use of
@State
) inside the View, it's purely for self-management.
For example, perhaps our Chess game has various themes — when one player
gets closer to a checkmate, their pieces start glowing. That's a particular
theme, and the View must use an @State
to toggle it.
But, the data indicating that a player is getting closer to a checkmate
lives entirely in the model. Thus, the @State
used to toggle the theme is
purely transient. Any @State
variables we see in the View don't store any
data about the player approaching a checkmate.
The Body Variable & the View
Recall the starting template of a Swift application:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
The body
var reflects the current state of the model, and it's the point
of contact between iOS and our application. iOS will not look anywhere else
for what to do. Additionally, iOS can ask for the value of that body
variable at any time. Now, we know that Views are immutable. This means
that there's no other way to change a View — how our application looks —
without changing the body
variable's value. Thus, every change we want to
render in the UI must be done by reconstructing the body
variable anew.
Because of this fact, frontend code in SwiftUI is declarative. This, does,
however, pose a challenge. If some data changes in the model, everything in
the View that depends on the changed data must return their body
variable.
How do we get those body
variables? Well, we could impose a rule: If a
change occurs in the model, all Views must return their body
variables.
This would ensure we get the body
variables, but it's horribly
inefficient. Some of those Views almost certainly do not render differently
because of the model's change. Thus, what we really want is a rule that
only asks for a body
variable from a View that actually depends on the
changed data.
This is the problem that MVVM solves. The whole idea behind MVVM is to ensure the following:
- Separting the application's Model and View,
- establishing a one-way connection from the Model to the View, and
- ensuring that only Views impacted by a change in the Model are rebuilt.
This is done by the ViewModel.
The ViewModel
The ViewModel has several duties job:
- Binding. The ViewModel binds the Model to the View, such that changes to the Model cause the View to react and get rebuilt.
- Interpreting. For some applications, our Model might provide SQL or network data. These forms of data can be complicated, and we don't want our Views performing data processing to render. Accordingly, the ViewModel can serve as a system for wrangling the data it receives from the model into a simpler format for the Views.
- Gatekeeping. The ViewModel also serves as a guard. It ensures that access to the model — requests for data or indications that data inside the model should change — is proper.
- Tracking. Ancilliary to the three roles above, the ViewModel must also track all of the changes in the model.
Because of the ViewModel's role, if the View ever needs data, it must send its request to the ViewModel. If the model should change based on user input, the View must send that input to the ViewModel.
From Role (4), the ViewModel has two behaviors: (a) It notices changes in the model, and (b) it publishes the fact that something has changed. Any View interested in that change can listen to these publications. This is similar to the observer pattern in object-oriented programming. The Views are all subscribed to the ViewModel, and the ViewModel is constantly observing the Model.
This pattern is followed because the ViewModel should not have special connections to its subscribers. It doesn't serve any one View — it serves both the Model and the Views as a whole. It's up to the Views to subscribe to the ViewModel. If a View (a) doesn't need data from the model or (b) doesn't need to send data to the model, it doesn't subscribe. If it does either of those two things, it subscribes.