Scalable modern architecture for iOS projects — Part 3
In previous articles, we covered the general definition of the architecture and then we started with the building blocks like coordinators and application context. In this article, we will elaborate on the actual MVVM part of the architecture.
There have been many flavours for the MVVM implementation since Microsoft presented it in 2005, and undoubtedly there will be many more. The one shown here is just another one that I’ve found fits iOS applications quite well, and significantly it simplifies testing considerably and provides consistency across the project.
What is MVVM?
Model — View — ViewModel is an architectural pattern, derived from Martin Fowler’s Presentation Model, that aims to separate the actual View or presentation layers from the business logic (or state) through a “converter” (the actual ViewModel) that manages the transformations for the presentation layer.
Ok, that’s the formal definition out of the way, now; let’s go into more detail into what it means for us as iOS developers. We usually associate the MVVM pattern with what is generally called clean architecture that nowadays is a must for any modern iOS application; this is not entirely accurate though, many patterns can achieve the concept of clean architecture, MVI, MVP, Viper and even Apple’s favourite (and the one we all learn with) MVC. The concept of clean architecture, which is my personal opinion, is not about the architecture to use in your application, but about following good engineering practices like SOLID, Inversion of Control, Testable code, etc. The actual architecture of the application is just one more (and probably needed) component to achieve that clean architecture goal, but not a magic recipe for it.
Our flavour of MVVM
This particular implementation of MVVM has three clear objectives:
- Allow for easy and quick testability, including snapshot testing.
- Enable the option of having different “Views” or user interfaces for different devices or use cases.
- Consistency, enabling any engineer to work in any part of the application, is fundamental in large engineering teams.
Worth mentioning, regarding the second objective, you won’t need that flexibility in some cases, and you can use Apple’s Adaptivity and Layout to achieve that functionality. But, unfortunately, I’ve found over the years that in large projects, if you need to have a good interface for iPad, for example, you’ll need and specific view for different size classes; SwiftUI has made this much more straightforward, though.
Let’s start with a diagram:
Let’s focus on the actual ViewModel, given that is the most crucial part of this implementation, you could implement that data layer and the view layer in many different ways, but as long as you follow the input and output separation in the ViewModel, you’ll maintain it’s benefits. As per the diagram:
1. The first arrow indicates the inputs from the View layer to the ViewModel; these are user (Taps, Selection, etc.) or system (Lifecycle, etc.) events that are passed to the ViewModel. In most cases, it generates some state change.
2. The second arrow indicates the outputs; these are the properties the View Layer needs to display the correct information to the user, for example, the screen title, the title in buttons, etc.
By separating these two paths into different “groups”, we manage to create a really simple public interface for the ViewModel, which in turn also allows us to test, using unit tests, almost all levels up to the View layer because any view interaction will be directed through the inputs of the ViewModel and any data displayed to the user, will go through the outputs of the ViewModel.
Bidirectional binding is critical for the proper separation of concerns between layers and is usually the part that most headaches produce, especially how to enable those bindings; here is where using RxSwift/RxCocoa and in SwiftUI applications Bindings and ObservableObject actually makes our job much easier; you could use delegates if you want to and achieve the same effect, but I’ve found out that makes the code much more convoluted and harder to follow.
Another nice reward of this approach is that it directly enables Snapshot testing (using the much faster XCTest framework), making our UI testing much easier and faster.
This is enough theory for a while; let’s see some examples of how this translates to actual code in an application. In the following example, I’m using protocols to organize and group the inputs and outputs, but this is optional; I think the syntax like `viewModel.input.viewDidAppear()` is neatly organized.
Hopefully, the comments in the code are enough to understand what each section is doing, but some parts are worth further analysis.
As per the code, inputs are simple Swift functions, with or without parameters, depending on the requirements. However, outputs deserve a bit more attention; we create a private `BehaviourRelay` which stores the actual value of the output, paired with a public `Driver`, which will be the actual output used to bind the UI element to it. As per the Rx Documentation, drivers can’t complete with an error, they are always observed in the Main Scheduler, and they replay the last value when a new subscriber is attached. These three characteristics make them perfect for View Binding and also testing.
Although we’ll go into more detail in the article dedicated to testing, by triggering an input (for example, `viewDidApper`) and then observing the value of the viewState output; we can be sure that the right data (when `.success`) is being sent to the screen, and with the use of a recorder, even that the screen goes through the right states (loading, success or error, etc.).
This formal separation in inputs and outputs can feel like too much boilerplate and probably not worth it for small projects. Still, it creates a clear separation of concerns in bigger ones and allows flexibility in the UI (we could use the same ViewModel with different views, one for iPhone, another for iPad, and even another one for macOS). Also, Xcode templates are great to make the IDE pick up that tedious task of setting up all the protocols and essential elements.
How do they fit with coordinators
So how the actual MVVM structure actual fits in with the coordinators we saw in the previous article? Let’s assume that we have the following elements:
- A coordinator called `WeatherCoordinator` has gained control from the AppCoordinator.
- A MVVM module composed of `WeatherViewControler` and `WeatherViewModel`.
- Another MVVM module composed of `CitySelectorViewController` and `CitySelectorViewModel`.
Logically, we want to present to the user the City Selector scene first, and once the user selects one, we show the actual weather details for that screen; the actual coordination will look like this:
We have a fully functional MVVM with coordinators flow that will allow us to have a really clean approach for navigation and data flow. The temptation would be to keep adding routes to the coordinator as we need to, but that will decrease the flexibility and increase the complexity. Instead, it is generally a good practice to keep the flows as short as possible, so they are easier to test and modify and increase the chances of reusing them in different parts of the application.
In the following article, we’ll start working closely with the UI of the application, especially how to create views in code in a simple manner on how to bind the different UI elements with the ViewModel using `RxCocoa`. At that point, we’ll be able to create an entire flow using these techniques.
If you missed the previous articles: