The RS letters are often used in the car industry to refer to the most powerful version of a given car. For MVC-RS, the same idea applies: it is MVC, but way more powerful, thanks to the 2 extra layers: Router and Storage.
The first version of MVC was introduced in the 70’s alongside SmallTalk to answer the growing needs for GUI apps. The new version of MVC, the one recommended by Apple to build apps, is slightly different. In this version, the View layer and the Model layer no longer know each other. The main advantage of having those 2 layers completely decoupled is that it makes them reusable. But what does it mean to have reusable layers?
Swift is reusable on all the Apple platforms, iOS, macOS, watchOS and tvOS but also on Linux and someday, on Windows and android.
Reusing the View layer is different from reusing some common piece of code, because Views are tightly coupled to the platforms they are running on. That makes sense, you are not rendering the same kind of UI and you are not allowing the same kind of user interactions on a watch, phone, tablet, computer or tv. If you have written a View layer on macOS, you have used AppKit and so, your layer will be reusable only where AppKit is available, which means only on macOS.
The Model layer doesn’t have such constraints as the View, so you should be able to reuse your Models on all the platforms where the language is available... Not really. The first common mistake you often see in iOS development is the usage of UIKit in the Model. Apple is not helping you there, because they do this very same mistake in the documentation and in some WWDC sessions. The problem with this is that you no longer can reuse your Model on other platforms. Let’s have a look at this example:
Here, you won’t be able to reuse your code on Linux for example. Instead, you should have written something like this:
And when using this in an iOS project, it’ll be the Controller’s job to convert that Data into a UIImage.
In iOS development, you should always follow this simple rule:
No UIKit classes in the Model
Now that you follow this rule, your Model layer should be reusable everywhere, right? Still not… Because the Model does not only modelize the domain you are interested in, it is also usually responsible of the communication with your Persistence layer.
On this example above, you have 3 differents database providers. If you’re using db A, you’ll be able to reuse your Model everywhere. But you might be also using db C on your macOS project and that database isn’t available on any other platform. In this case, your Model won’t be reusable on the other platforms.
The problem here is that when you think about MVC, you always think only about those 3 layers: Model, View & Controller. But in fact, there is an implicit 4th layer: the Persistence (which is usually a local file, a local database or a distant call). If your Model knows your Persistence, it’ll be reusable only where your Persistence is available. To avoid this, let me introduce you to the S layer of MVC-RS, which stands for Storage.
The Storage layer will simply go between your Persistence and your Model and it will have 2 tasks:
- Talking to the Persistence
- Converting raw data from the Persistence into entities of your Model
Talking to the Persistence
This task is more complicated than it seems, because calls to a Persistence layer are asynchronous, they might succeed but they might also fail. Your Storage layer will have to take all those parameters into account.
Your Storage layer will convert the raw data it gets from your Persistence into entities of your Model when you’ll read data that your app will present to the user. But it should also be able to do it the other way around: when your app need to persist some elements, your Storage layer should know how to convert an entity of your Model into raw data that it will send to the Persistence. It is important to keep in mind that both those tasks might fail.
In the examples of this article, you will see the simple Car Model from previously and the pretty handy and common Result enum pattern rather than the heavy do-try-catch syntax to manage errors.
Here is an example of what you might find in the Storage layer.
The reading methods don’t return instantly because calls to the Persistence are asynchronous. They take a closure as a parameter that will be called once the Storage work is complete. If something failed while talking to the Persistence or while converting raw data into Model entities, the onComplete closure will be called with an error, otherwise it will be called with the appropriate Model entities.
You might find different reading methods, such as one that takes an id to fetch a single entity of your Model, or another one that takes some parameters in case you don’t want to load every instances of your Model.
The writing methods, create, update & delete complete this CRUD example. They have the same signature: they take an entity of your Model and an onComplete closure. If something failed while converting your Model entity into raw data or while sending that raw data to your Persistence, the onComplete closure will be called with an error. Otherwise, it’ll be called with nil.
Defining your Storage as a protocol is pretty convenient as it allows you to write different implementations for different kind of Persistence. You might find one for a sqlite database, another one for a Couchbase database, one for some Rest API calls… You can also write an implementation for testing purpose, if you want to mock your Persistence.
Thanks to this Storage layer, your Model layer, now, only modelize a domain. It’s no longer linked to your Persistence layer and thus can be reused everywhere. Your Storage layer, alongside your Model, can be reused as well, but only on platforms where the same Persistence is available.
What allows MVC?
MVC allows to answer a number of use cases. Those use cases might be something like show me a list of cars, show me a detail view for this car, update this car, delete this car… But MVC doesn’t tell you on which use case your app starts, nor does it tell you how to transition from a use case to another. So usually, you write things like that:
This CarsListController, as its name implies, should be only responsible for rendering a list of Cars. But what are you doing here? You instantiate the next Controller and you push it on the navigation stack. But how do you know that there is a navigation stack? This Controller shouldn’t know that. And by creating the next Controller, it decides what happens after him. So this CarsListController, who should only render a list of Cars, knows what is above him and what is after him. That’s a problem.
You don’t often see this kind of signature in the Controller layer in iOS development:
Usually, it looks more like this:
The Controller is initialised with nothing and, in the viewDidLoad, it’ll ask the Persistence (through the Model usually) to get the data it needs. In a way, it makes sense. If the CarsListController doesn’t ask for the data it needs, who’s going to give it to him?
But the problem when you do that, is that this Controller must be able to render different views. As the call to the Persistence is asynchronous, it’ll have to show some kind of spinner while waiting for the response. If the call to the Persistence failed, the Controller should display an error message to the user. If the call to the Persistence succeed, it might be empty, the Controller should show an appropriate message and maybe a retry button. And finally, if everything went right, the Controller can show a list of cars!
So this poor Controller has to manage 4 states:
The Controllers know too much: they know who’s above and who’s after them. The Controllers do too much: they manage a loading state, an error state, an empty state and finally the actual work they were made for initially. No doubt that you easily fall into the Massive View Controller trap!
To fix this, let me introduce you to the R layer of MVC-RS, which stands for Router.
The Router is a layer that goes ‘above’. It knows your Storage, your Model and your Controller layers, and it has 3 tasks:
- Communication with the Storage
- Coordination of the Controllers
- Management of the chrome
Communication with the Storage
The Router being in charge of the communication with the Persistence, through the Storage, means also that the Controller layer has no longer any link with the Persistence. Your Controllers won’t ever again ask for data, it is always the Router that will give data to the Controllers. So it’ll be Dependency Injection all the way for the Controller layer.
The code below illustrate how the Router will talk to the Storage when you want to show a list of Cars.
First, the Router knows that asking data to the Storage is asynchronous, so it’ll start by showing a loading screen to the user. Once the call is complete, there is several scenarios:
- An error occurred, the Router will be responsible to show an appropriate error view.
- The result is empty, the Router will be responsible to show an appropriate ‘empty’ view.
- You have actual data, the Router will instantiate the appropriate Controller with that data and show that Controller.
The 4 states (loading, error, empty, loaded) we saw previously are now managed by the Router.
Coordination of the Controllers
I chose the coordination word here on purpose because it is very closely related to the Coordinator pattern that was introduced by Soroush Khanlou at NSSpain 2015. I highly recommend watching that talk if you haven’t already (even though it’s written in a strange old language 😝).
The idea is that the Router will be the one telling how to transition from a Controller to another. To achieve that, you need to change little things in your Controllers.
If you take this example from before, you no longer want that the Controller decides what will happen next. One convenient way to do that, would be to ‘delegate’ that task using a simple closure.
The Router will set that closure as it is the one responsible for the transitions between the Controllers or what you would call routes in other contexts, thus the name of the layer.
The chrome is everything that is outside of your Controller. In iOS development, the most common examples of chrome are the navbar and the tabbar.
Trying to set a navbar button from a Controller is the same kind of mistake than trying to push on a navigation stack from within the Controller. A Controller shouldn’t be aware of what’s above him.
But from the Router, it’s totally fine. The ‘show’ method we saw in the showCarsList example, could simply be implemented like this:
So your Router knows that there is a navigationController, it is then completely ok for him to set a navbar button.
The same kind of idea applies for a tabBarItem.
Splitting the Routers
In the examples you’ve seen so far, a single Router is enough. But with a bigger app, splitting the Router layer into different classes might be a good idea. You can do this by having a Router by entity of your Model. So if this app was growing and managing Drivers alongside the Cars, you would have a DriverRouter as well.
You can also regroupe Routers by portions of your app, so you might have a SignupRouter or a SettingsRouter. Both ways have pros & cons, the key idea is that you should split your Router at some point, you don’t want to trade Massive View Controllers for a Massive Router instead 🤓
Advantages of MVC-RS
First of all, it is an MVC-based pattern, which means that most of developers know already the key concepts. It also means that it is easy to convert an existing MVC project to an MVC-RS one.
You can reuse your Models on every platform where Swift is available and you can easily switch the Persistence layer you are using.
Deep linking can easily be implemented, all you need is a function that’ll map some urls to some methods of your Router.
Your Controllers are now very light and very dumb, they do what they have to do and that’s all, no extra work, no extra knowledge. And say farewell to the Massive View Controller issue, you won’t ever see that in an MVC-RS project.