I’ve seen many developers and articles blaming the Model-View Controller architecture for many, if not all, of the troubles that are brought up in the iOS community.
Today, I offer a few alternate views and approaches to this issue. Moreover, we also explore a few techniques/guidelines that can reshape the concepts of MVC as well as project management in general.
You’ve probably seen this diagram from Apple’s official documentation:
MVC seems pristine in theory, just like the diagram shows.
The view allows users to interact with it and user actions are captured using the controller, which then updates the view accordingly. In case a user action requires an update to the model, the controller waits for the update completed notification from the model, after which the view is updated.
Seems simple enough and everything should work as intended, right? Hundreds of thousands of applications have been written using this approach, even large ones. In fact, not only iOS and MacOS apps, but Web applications, Windows applications, and others have also been using this approach for close to four decades.
One of the most touted advantages of MVC, especially within iOS, is ease of use. UIViewController and its successors in the UIKit framework give a lot of functionality and opportunities out of the box. In fact, you don’t even need any programming skills to write smaller apps. Anyone can have an app up and running after following a few YouTube lectures for a couple of days.
Many applications still use this approach today and don’t seem to run into any issues. So, where does it all go wrong?
Problems begin cropping up when you’re working on relatively larger projects. Evidence and experience show that MVC scales very poorly, which leads to a critical and well-known issue called the Massive View Controller.
The Core of the problem
I see two core reasons for this issue:
- MVC doesn’t really give clear instructions on what entities and classes you need to create and which ones you don’t — especially in iOS. UIKit MVC initially only gives you the UIViewController, and that’s basically it. The view exists, but is not declared initially.
Hence, even when you don’t create a separate view instance and have only the UIViewController, the application will perform its functions. It’s the same with the model. The structure and architecture of the model, as well as its interaction with the controller, rely entirely on the creativity and imagination of developers.
This forms the basis for the second reason.
- Due to poor understanding of the domain area and the inability of developers to allocate necessary entities, we often see haphazard accumulation of dependencies and functionalities within the UIViewController.
Here, when the developer adds new functionality, instead of creating new entities and refactoring the existing architecture, it is simply added to the ViewController. Hence, we can safely blame lack of proper architecture and inattention / negligence / ignorance / inexperience of developers as well for the issues we see in MVC.
Tackling the First Problem
The approaches of MVVM, MVP, VIP, VIPER, Flux (Facebook), Riblets (Viper from Uber), Clean-Swift, and others, partially or completely solve the first part of the MVC problem — making them quite popular over the past few years.
Many of them give developers clear instructions about the classes and details cases when it is necessary to create them. They also make it much easier to work in a team, especially if the team consists of 10+ developers who work on the same project (for example, Uber works 150, JustEats 20+, Facebook and i.e.). It means creating a large number of isolated objects, only to cover the Controller code, which in turn allows you to allocate work within the team without losing time.
Handling the Second Problem
I have seen many projects using VIP and MVVM with view-models and presenters classes containing more than 1500 lines of code — featuring multiple operations, from parsing and mapping to querying databases and making http requests.
The problem of developer inexperience or lack of understanding of the application architecture in general is not solved by simply changing the architectural pattern. In my experience, there really isn’t any “golden bullet” that will allow you to maintain / write large and complex projects “by default.” It will always require effort, a unit of tests, competent planning, and time.
What can we do?
1. Update the ViewController
First off, no object in the application should be responsible for more than 1 function (Single Responsibility).
For many developers this might be obvious, but many others still don’t adhere to this simple guideline. For many, it’s not obvious that UIViewController is the same object as the others, and should be accorded only one responsibility – i.e. linking model and view. It should support the view in the corresponding model state and notify the model when it needs to change this state. Anything that goes beyond this definition, in my experience, does not belong to the controller.
All other fluff like animations, layout, composition view, changing drawings, parsing, mapping, http requests, queries to databases, different levels of the operating system services, etc. do not belong to the controller. So, avoid creating or calling any of these within the controller.
Another standalone item should be navigation. If your ViewController has more than one exit point, then you should consider those points and navigation control. We have often found ourselves tackling ViewControllers with 5-10 exit points scattered throughout the controller code.
There are many ways of organizing this — including breaking down the navigation into individual extensions, managing navigation exclusively through segue, creating a separate Router entity, inheriting the UINavigationController, and so on.
Just keep in mind what the ViewController should do and what it shouldn’t.
The same can be said about the entities that are used in MVVM, VIPER (the list is much longer). If it’s a View-Model, it’s only a representation of the data for the View. On the other hand, if it’s an entity, then storing and converting data and so on. Before you add a new method or property to an existing class, always consider whether it really belongs in that class.
2. Model Isolation
One of the most common errors I see in iOS is insufficient isolation of the model level. In fact, I find it best to keep models in separate frameworks.
For example, if you have a food delivery application, then you can render restaurants and other related items (search for restaurants, menus, location, etc.), dishes, delivery areas, etc. in different frameworks. Similarly, for a flight booking application, you can allocate a dozen frameworks (search for tickets, reservations, purchase, history, etc.). Each framework can then be covered by a separate unit test suite.
Of course, you can choose not to do this, but even in that case, you should try writing models that are completely isolated from any interaction with the controllers.
3. Avoid Massive UIViewControllers
I have seen huge storyboards in addition to huge UIViewControllers during the course of my work. They need not always be huge in terms of the length of code, but sometimes it’s just about the number of UI elements they house.
Developers should ask themselves a simple question when they encounter a huge list of ViewControllers: “Can I somehow break this VC into smaller Views or VCs?”
Handpick elements on a screen that perform a logical operation or feature for the user in separate ViewControllers. This next screen is an excellent example.
Here, each cell in the table is a separate VC. The title with the Author’s avatar, author name, and time is a separate ViewController.
The content itself and the link is in a separate ViewController.
Finally, the buttons ‘Like,’ ‘Comment,’ and ‘Share’ are housed in yet another ViewController.
4. Avoid using a lot of UI code
Using lengthy code to implement simple UI components through the ViewController will inevitably lead to a clunky architecture that just won’t scale past a point. We can solve this by ensuring the default inheritance of UI components instead of customizing them within the ViewController. Alternatively, you can also add extensions to define the UI elements.
5. Avoid Massive Storyboards
I’ve encountered massive Storyboard files in many projects over the years. They’ll typically contain 20-50 ViewControllers with varying levels of detail.
These projects almost always start off smoothly. There’s no hiccup and everything seems perfect as the completed application chugs along. However, this calm only lasts until project requirements change or new developers are added into the mix. Once this happens, it’s complete bedlam thanks to the complexities involved in figuring out the massive storyboard.
One solution involves not using the Xcode UI Builder at all and creating the entire layout using code. You can use various libraries to simplify writing constraints (Parus, Masonry, etc.) or simply write everything yourself.
However, if you must use Storyboard, then try and break your project into several storyboards. A Storyboard should ideally have a single responsibility or use case. For example, if you have a Cab hailing application — the individual Storyboards can be: Hire Cab, Tracking, Payment, General Information, and so on.
Those, less independent features with storyboard reference, doing this is very convenient and requires a minimum of code writing. Limit the number of screens in a single story board to three (3).
The larger the storyboard becomes, the more resources needed to run it smoothly. In addition to making it harder to figure out and break down, it takes slows down loading times. Moreover, any newer development teams will have a hell of a time figuring out a large storyboard — slowing down development.
The point of this post was to address the tendency of accusing MVC for so many developer issues (especially iOS developers), when in fact, it simply isn’t the root of all problems.
More often than not, developers will face similar issues even when moving to other architectures. Of course, those issues might take longer to crop up on different architectures, but they will show up sooner or later.
The Massive View Controller problem crops up in 90% of problematic cases and this isn’t because MVC can’t scale. But it’s because the original developer(s) didn’t follow guidelines that would help MVC scale smoothly.
Although VIPER, MVVM (and others) can help you get around many of the issues associated with MVC, they’re mostly not the panacea for all issues you might be facing. Some of them can simply be associated with not-so-optimal development practices.
I’ve simply tried to cite some examples and situations where MVC might not be to blame. Nonetheless, I’m simply playing devil’s advocate here as I myself use VIPER / MVVM on my projects.