MVVM - you are doing it wrong
Application architecture is a hot topic in mobile development and there is a reason for that - every application needs some logical form of structuring code to remain reliable, scalable and maintainable. iOS applications are not different. One of the most popular architectures is Model-View-ViewModel, where view controller and view fall under View part, while view model is the unit responsible for the business logic of our app. I have been using this architecture myself, have seen multiple implementations from other people, but none of them seem satisfactory, they miss something. None of those root components are perfect for performing navigation nor controllers creation. It feels wrong to do it the MVVM way where the controller is taking care of those tasks, but do we have to?
One of the possible solutions for handling routing in the world of MVVM is having view models expose an interface that tells the view controller when and where should it route to. However this solution is far from ideal - it makes view controller aware of its place in the application, reducing our ability to reuse it later on. A much better way of handling this is by introducing an additional component, that is missing from classic MVVM. There are two commonly used objects - Router and Coordinator. Both of them are valid solutions, they make some parts of unit tests very simple, however, there is one key difference - router manages routing from a single instance of the view controller, while coordinator takes care of entire flow. Which one is better? As always - there is no silver bullet solution that will fit all applications. If your app has a lot of independent screens, which can be presented in different contexts - you should probably use routers, if it has screens that can be grouped into few controllers-long flows - coordinators might be the better solution.
The application which I am working on right now falls into first category, so I have been using routers. Let me explain why are they such an improvement over routing without them. Bear in mind that most of the following attributes are shared with coordinators.
Router's interface doesn't need to know anything about UIKit - the controller that it uses could very well be just a protocol that exposes basic methods like push, present and dismiss - thanks to that, routers are easily testable and can be used regardless of platform or device.
Router's navigation interface is the only interface. If you perform routing with view controller, you are dealing with tons of methods that you may not even be interested in - meanwhile, router's interface remains very small, very simple and fully testable. While you may not necessarily need to test routers, this simplicity enables cleaner view models tests, since call to the router will often be the outcome of complicated view model's logic.
To fully benefit from routers, we need to inject them into our view models. We achieve this by using protocol for each router, and this unlocks more amazing traits:
There are some common UI-related operations that will be performed on almost every screen, like the presentation of activity indicator and error/success messages display - those can be placed in some root protocol for all routers, like RouterType so we can avoid a lot of unnecessary code duplication.
Common router functionalities can be encapsulated in protocols with default implementations and composed. For example, we may want our router to be able to present SafariController with some url - this is not something that all routers need to do, but we might be using it in a few places. All we have to do is create a protocol with default implementation - and it can be further composed with other protocols like ImagePickerRoutable or DocumentBrowserRoutable.
Usage of the router has one more amazing trait - it makes extraction of common logic, related to navigation, extremely easy. Let's say that you have a lot of alerts, action sheets or popups that require some action from a user, then perform some task and close. If some UI actions should be performed afterward, like the presentation of activity indicator or another controller, view model will normally inform view controller about it. Now if same actions need to be handled in multiple places of application, we can easily extract logic into a single, reusable unit. But what about handling those navigation-related actions? If they are handled by controllers, all of them will need to do it - that's potentially tons of code duplication and just a waste of time. With router this issue simply doesn't exist - we can just reuse it along with some common Handler.
There are also some good view model related practices I would like to mention here:
View model should not be the data source, but it should rather expose data needed for cell configuration - this is especially useful for testing complicated tableViews and collectionViews. You should also create data sources as separate objects - they can be easily reused in future and there is no harm in doing this right away.
Data that is being used to populate certain view, for example UserTableViewCell, should be wrapped into a single structure - like UserCellConfiguration. This structure is just a thin layer between actual data and its transformation that is used to fill all the labels, imageViews and so on. It makes the distinction between actual model and view easier.
Use dependency injection - it enables usage of mock objects in your unit tests, making them easy to write.
View model should not import UIKit - this isn't something that is crucial, but if you keep this in mind, it will help you with keeping the separation between UI and logic layer.
How does it all work together?
Apart from introducing routers, I have also been using one more component in my app - ControllersBuilder. ControllersBuilder is just a simple structure capable of creating all controllers in the entire application.
Every build method follows the same scheme:
- Initializes router, injects itself, so such router can request a creation of another controller
- Initializes view model, injects required dependencies, initial data and router
- Initializes view controller, injects view model
- Assigns view controller to weak property of router
The important thing to note here - it's better to have builder methods return plain UIViewControllers, rather than an instance of the specific subclass. Same goes for router - it is better to have it hold a reference to UIViewController. It is not mandatory by any means, however, this is the way I have been doing it so far and it helps me with keeping those objects as dumb as possible.
One of the biggest benefits of router and DI introduction is how it improves unit tests. I would like to show you how does one of them look like in my project:
Router is just a protocol injected to view model and thanks to the dependency injection we can also mock service responses, making our tests extremely easy to write and read - test above is not an exception, it is just a part of MVVM-R's reality.
Here is another example. This time presenting snapshot test:
This is simply brilliant - thanks to DI and ControllersBuilder, we can snapshot test every single screen in app, in just a few lines of code.
I have been using this architecture to a great extent in production-ready application for the past five months and I am really satisfied with how it improved the quality of my work:
- Responsibilities are well spread across components,
- Dependencies are injectable,
- Adding new dependencies to logic units is as easy as extending typealias by another protocol,
- Controllers are reusable,
- Introducing modifications to existing code can be done painlessly and entire logic is well tested.
Overall, it is just a pleasure and fun to work with.
Results? This application has been successfully released to the AppStore and with almost 8 thousands users, we have managed to achieve close to 99% crash-free rate during the first week, registering single crash only. As a developer, I must say - this architecture and the testability it enabled, gave me a lot of confidence when it comes to releasing new features or refactor.
Disclaimer - I did not come up with described architecture myself, it was introduced to me by my colleague at work, Szymon Mrozek, so huge thanks to him. You can check out his articles here.
And here are some other links that you may find useful:
- Protocol composition - great article, which introduced this concept to me. This is exactly the way my app is handling dependencies.
- Coordinators - introduction to router's alternative concept, which I have mentioned earlier.
- App Architecture - great talk about architecture in general, with some major points highlighted, some of which can benefit your application straight away.
- Sourcery - tool for generating boilerplate code, I am using it to create all mocks.
If you enjoyed this article, let me know what you think in comments below.