Many design patterns guide iOS architecture. People have varying opinions on what constitutes good app architecture in iOS.
I, for one, have issues with all the modern iOS architecture “best practices.”
Their main problem is that they only focus on massive view controllers. While this is a problem that we need to address, it’s far from being the only one.
After years of working in many teams with diverse backgrounds, I created the Lotus MVC Pattern to address these issues.
I have taught this new pattern to my students and email subscribers for some time now. I recently presented it at MobileFest in Kiev, and this is the first time I show it publicly in a detailed article.
The problems of
popular iOS design patterns
In this section, I will analyze in detail the most popular iOS design patterns: MVC, MVVM, and VIPER.
But I will look at them from a point of view that I rarely see discussed. This will highlight the five problems an architectural pattern needs to address.
What happens when you blindly follow iOS app architecture “best practices.”
Years ago, in 2011, I worked for an agency that made apps for big clients. One of these was an investment bank in Amsterdam that wanted an iPad app to allow clients to browse their portfolios.
It was a quite complicated app, with many screens, charts, and interactive features.
Instead of the classic REST API returning JSON, I had to work with SOAP API and parse XML data. And, since it was a banking app, it needed extra security features.
Banks are very risk-averse businesses, and rightly so since they manage people’s money. So my final code had to go through a third-party review before the app went live.
Even though at the time I had only three years of experience in making iOS apps, I felt quite confident in my abilities. I followed all the “best practices” of the MVC pattern, so I expected the reviewers to recognize the brilliance of my code.
But the verdict was different than I expected.
Although the code base was solid and the app worked well, the conclusion was that my code was:
- hard to read,
- hard to maintain, and
- hard to test.
At the time I dismissed the criticism.
For starters, unit testing was not widespread at the time in iOS, so who cared about testing? Xcode did not have any support for it, and you had to use mocking frameworks like OCMock.
On top of that, most iOS developers thought that in Objective-C we wrote better code. Reading those blogs, I was convinced of this myself.
So I thought that the reviewers didn’t understand how things went in iOS development.
It wasn’t until two years later that the first Objc.io issue on lighter view controllers came out and I realized that I still had a lot to learn.
I know, shocking.
After years of experience in iOS development, many apps under my belt, plus years of teaching, I finally recognize why that code was so hard to read, test and maintain.
The classic MVC pattern diagram and the problem of massive view controllers
The cause of that messy code was the classic version of the Model-View-Controller (MVC) pattern.
How many times have you seen this diagram? Personally, too many.
As the joke goes, MVC does not stand Model-View-Controller, but for Massive View Controllers. I guess there was a time when that joke was funny, but it has become quite trite lately.
But indeed, that was the problem in that project back in 2011. Most of the app’s code was in monolithic view controllers. So, I do believe that this is the main problem in iOS architecture.
But not in the way you probably think.
To me, the problem of iOS design patterns is that they focus only on the massive view controllers problem.
What hides behind the joke is the classic separation of concerns design principle. This is a problem in software development in general.
But it’s not the only problem of the MVC pattern.
The arrows in a design pattern diagram are more important than you think.
Notice in the diagram above the “update” arrow going from the model to view controllers. Many developers ignore that arrow, but it is essential. What it shows is that the execution flow can move up from the model layer into other layers.
Apple introduced its version of the MVC pattern at the time of Mac development. Back then, Swift, with its value types, did not exist. Model types in Objective-C were full-fledged objects.
On the Mac, it was typical to connect code using Key-Value Observing, a mechanism that triggers the execution of code when the value of a property in an object changes.
This meant that changing the state of a model object could trigger code in the observers, namely view controllers. That’s why the MVC diagram above includes that arrow.
And that’s just the start of our problems.
The many responsibilities of the model layer in the MVC pattern
The usual MVC diagram hides an important fact. In any app, there are many MVC stacks, one for every view controller.
Here the arrows going from the model layer to the view controller layer gets a new meaning. Since the model is made of objects, it also gets the responsibility of being the repository of the app’s global state.
Any app has a state which view controllers need to access. A model made of objects means that all view controllers reference the same instances. Since the model layer updates view controllers, any change to the global state propagates upwards.
When any change to the app’s state could trigger the code in other view controllers, you have a much harder problem at hand than massive view controllers.
But it does not stop there.
Model objects often access storage too, getting overloaded with responsibilities. It’s not uncommon for an object to persist its data on disk. As an example, Core Data managed objects have a reference to their managed object context.
After all, when you only have three layers, you have to put that code somewhere.
The execution flow of code in complex object graphs
Another necessary piece of an iOS app’s architecture is view controller communication.
As view controller succeed each other on screen, they need to exchange information.
For example, selecting an item in a table view often triggers drill-down navigation. The next view controller displays the details of the selected item, so it needs to know about the user’s selection.
View controller communication introduces tight coupling between view controllers. This adds complications to the already complex execution flow of MVC.
Finally, most modern apps need parallelism in one form or another. A typical instance is asynchronous network calls.
The MVC pattern handles networking with a central networking singleton, which the general diagram does not show:
It’s clear that in an app with an already complex execution flow, parallelism is bound to introduce more complications. That’s even worse when you use singletons, primarily if you use KVO for those too.
In general, singletons are considered an anti-pattern that you should avoid. Still, they are popular in standard implementations of the MVC pattern.
The few layers of the MVC pattern exacerbate architectural problems
So, to recap, we have five essential aspects to keep in mind when architecting an iOS app:
- separation of concerns;
- state sharing;
- state propagation;
- view controller communication;
You cannot run away from these.
The problem is that, when you only have three layers in the MVC pattern, you have to think about all these all the time. The smallest piece of code can have far-reaching consequences.
Now you see what I mean when I say that massive view controllers are not the problem you think they are. They are just one of the aspects you need to consider in any app.
The problem is focusing only on one aspect, ignoring the others.
The MVVM pattern rehashes MVC without making any substantial change
The MVC pattern has, clearly, its problems. That’s why new design patterns were introduced, allegedly to solve such problems.
I don’t think they do a great job, although they do bring some interesting ideas to the table.
One of these is the Model-View-ViewModel pattern (MVVM), which is usually represented like this:
What the MVVM pattern does is add a new layer, the view model.
The idea is to put all the code that usually goes into view controllers in the view model layer instead. View models the pass data to view controllers in a format ready to be displayed on the screen, relieving them from responsibilities.
If you ever wondered why the acronym does not mention view controllers, that’s because they are part of the view layer. In MVVM, view controllers have no responsibilities and don’t take decisions. Instead, they behave like passive views.
Knowing that, compare it to the MVC pattern. As Soroush Khanlou put it very well in this article:
You don’t need an advanced graph theory class to see that these are almost completely identical
Moving responsibilities to another layer does not solve much. MVVM replaces massive view controllers with massive view models.
The proponents of this pattern say that view models are more testable than view controllers. I honestly fail to see how.
But there are other ways in which MVVM is the same as MVC.
How MVVM is even more like MVC than you think
Let’s rearrange MVVM in multiple vertical stacks, as we did for MVC:
In this diagram, I am using a variation of the pattern, called MVVM+C, which adds a coordinator. That, in my opinion, is a good idea for view controller communication. We’ll get back to it later.
Again, we see that MVVM is the same as MVC:
- There is still an arrow going up from the model layer to the view model layer. In MVVM the model layer is also made of objects. This makes the model responsible for state sharing again.
- View models observe model objects, so state propagation also works as it does in MVC. MVVM usually uses reactive frameworks instead of native mechanisms like KVO, but there are also flavors of MVVM that don’t use RFP. In the end, though, it makes no difference. State propagates through a binding mechanism. In fact, if you dig into reactive frameworks, you can see that they use KVO. You can see that in their documentation too.
- The network controller is still a singleton. Hooking it using a reactive framework again makes no difference from an architectural point of view. Observing singletons only adds problems.
Yes, I know: these are not absolutes. There are differing opinions and implementations when it comes to MVVM. Still, I checked many popular implementations, and these main ideas are widespread.
Finally, MVVM has, for me, an extra problem: it rejects storyboards and segues.
Some developers seem to be against storyboards. I wrote an extensive list of reasons why, in my opinion, that aversion is not justified.
There are MVVM implementations that use storyboards, but for the UI of view controllers. They don’t use segues.
In my opinion, storyboards without segues make no sense. You might as well use Nib files and spare yourself the problems of a large storyboard file. You get the benefits of storyboards only when you use segues.
How VIPER hides familiar concepts behind uncommon terms
MVVM seems to be an unsatisfying variation of MVC. That’s where VIPER comes in.
The acronym stands for View-Interactor-Presenter-Entity-Routing (that’s quite a mouthful). The pattern is usually represented like this:
This looks completely different from MVC and MVVM. In my opinion that is, in and of itself, a problem.
The reason is that VIPER is a backronym where the names of the layers were chosen to generate a cool sounding name.
That comes at the cost of hiding the details of the pattern. Looking at the diagram, the only familiar piece seems to be the view layer.
One can’t also help wondering where the view controllers are since you can’t avoid them in iOS. Or, where is the R exactly? In the diagram, noting starts with that letter. But wait, why is the data store in the diagram but not mentioned in the name?
VIPER brings new ideas to the table at the cost of rigidity
To understand how VIPER works, it is useful again to redraw the diagram vertically:
I’m sure you can now place each component in it conceptual position more easily.
- The entity layer is the model layer, renamed. But VIPER has a fundamental change: there is no arrow going up anymore. In VIPER, entities are inert. Changing them does not trigger the execution of other code.
- The app’s state is also taken out of the entity layer. That’s what the data store is for. This is an excellent choice, since it separates responsibilities, making entities easier to use and to test.
- Like in MVVM, view controllers are part of the view layer and have no responsibilities. But where we had view models, we now have two layers. The presenter, which manages the UI, and the interactor, which encapsulates the core, low-level functionality related to a view controller.
- Wireframes have a role like that of storyboards. All the code related to the app’s navigation structure goes there. Together with presenters, they are part of the Routing layer (that’s where the R in VIPER is), and they manage the flow of view controllers on screen and their communication.
Even though I am not a fan of VIPER, I think that it’s an improvement over MVVM. It contains many useful ideas that I adopted too, even before I knew about the pattern.
It also comes with parts I don’t like though:
- It still rejects storyboards and the central role of view controllers.
- Like MVVM, it’s too rigid and prescriptive. MVVM prescribes a view model for each view controller. In VIPER, you always need a presenter and an interactor, even for simple screens that do not require such sophistication.
- It encourages the proliferation of protocols to connect all these components (this is not evident in a diagram). While there is nothing wrong with using protocols, it can get to the extreme. When you get to the point that you need to synthesize protocols automatically, to me, you got too far.
- VIPER spreads some responsibilities among more than one layer instead of placing them in a single place. Routing is such an example. This can be considered a violation of the separation of concerns principle.
The Lotus MVC Pattern
The MVVM+C and VIPER patterns introduce new unique problems, but also good ideas that we should not throw away.
After working for years on many apps and with developers coming from other platforms, I put together all the best ideas I gathered in a new design pattern: the Lotus MVC Patter.
The main issues of MVVM and VIPER
All the popular patterns, in my opinion, fall short on their promise of solving the problems of MVC.
In short, my main gripes with MVVM and VIPER are that:
- they are too rigid and prescriptive;
- they fight against the platform, rejecting the centrality of view controllers and storyboards;
- they don’t respect the separation of concerns, despite their focus on the massive view controllers problem. In MVVM view models have too many responsibilities, while in VIPER these are spread thinly across objects.
Still, we should not reject their good ideas.
View controllers tend to grow too much, and there are many responsibilities that we should take out of them. To list a few: navigation, data formatting, state preservation, access to shared resources, etc.
A modular architectural pattern for iOS apps
In the past ten years, I worked on many apps: personal projects, in jobs for different companies, a startup I co-founded, and for clients.
This exposed me to many developers that had a broader background than just iOS. This gave me access to many different ideas coming from other languages and platforms.
I also read a plethora of online articles, books on architecture, functional programming and networking. I even studied other languages like Haskell, the language that everyone talks about but nobody uses, as one fellow developer once wittily put it.
This resulted in the Lotus MVC pattern, which brings all these ideas together.
I didn’t invent any of these ideas. I just organized them in a single pattern.
The name of the pattern is not an acronym. It merely comes from the final shape of the diagram. I had to find a cool name for it, and if they can call a pattern VIPER, I can call mine Lotus.
The name includes the MVC acronym because that’s its foundation. I expanded it to take into account the five aspects of an app’s architecture that I listed above.
The result is a modular pattern that:
- respects the platform;
- follows the SOLID principles of OOP;
- is not prescriptive and allows you to implement only the parts that you need;
- separates responsibilities clearly;
- provides testable components.
View controllers remain at the center of the pattern and guide the behavior of the app
The first, fundamental aspect of MVC that my pattern preserves is the centrality of view controllers:
Let’s be honest. There is no escaping from view controllers in iOS. That is unless you want to reimplement all the functionality that they provide.
Believe it or not, I worked on projects that did precisely that. It was hell.
View controllers connect to views with the standard iOS mechanisms: outlets, the target-action pattern, and delegation.
They are not part of the view layer though, and they are not inert like in MVVM or VIPER. They take an active role at the center of the pattern and dispatch events to all other objects.
Model types are inert value types
I have been an advocate of this point for a long time. The model layer should be made only of value types.
I took the idea from this excellent presentation from Andy Matuschak, which I recommend watching:
You can see in the diagram that there is no arrow going up from the model layer. That’s because value types are inert. Objects in the app get their copy of a value, which they can change safely.
Value types are also easier to reason about since they can only have one owner. There are no callbacks, no delegation, no KVO, no notification, and no asynchronous code.
The responsibilities of the model layer are limited to:
- Representing the data of an app. For example, a banking app would have structures representing accounts, transactions, credit cards, etc.
- Encapsulating the domain business logic, which is independent of the app. For example, in our banking app, accounts, transactions and credit cards interact with each other according to clear business logic. This would be the same in other apps, for example in the Mac or web counterparts. This makes model types reusable across projects.
- Converting data across formats, for example, JSON, property lists, raw bytes, protocol buffers, etc. In fact, that’s the approach that the Codable protocols introduced in Swift 4 follow. All code related to encoding and decoding goes into model types.
Model controllers support view controllers and access shared resources
If you read Apple’s description of the MVC pattern, you find this two sentences:
One can merge the MVC roles played by an object, making an object, for example, fulfill both the controller and view roles—in which case, it would be called a view controller. In the same way, you can also have model-controller objects.
The first sentence refers to view controllers, which we already know. But the second one introduces the concept of model controllers, which I never see mentioned. Model controllers are objects that deal with the model.
This is what MVVM and VIPER try to do with view models, or presenters, interactors, and data stores, respectively.
The difference here is that model controllers are not rigidly defined. Instead, they are a layer in the pattern that you fill with objects with clear, single responsibilities, such as:
- preserving the app’s state;
- interacting with storage technologies like the file system, Core Data, SQLite, etc.;
- reading device sensors like the GPS, the gyroscope or the accelerometer;
- scheduling network requests;
These are all model controllers that view controllers need to share since they are the single entry point to some resource (disk, sensors, network). Sharing model controller instances happens through dependency injection (more below).
But some dedicated model controllers are not shared. These support view controllers, encapsulating well-defined responsibilities. Some examples are:
- data sources for table views or collection views;
- state machines for complex view controller logic.
View models format data to be displayed on the screen
One of the “raisons d’être” of view models in MVVM is to convert the data to formats that the app can show to the user.
The problem though is that, in MVVM, view models also do a plethora of other things.
But the idea is sound:
Like model types, view models are inert value types. Their role is limited to:
- formatting data to be presented on the screen;
- transforming the user’s input back into the app’s model types.
I have a full article explaining this version of view models in detail.
This idea of two-way transformation is also not new. You can find it in value transformers, which existed in Mac development since Mac OS X Panther, released in 2003. Yes, it’s that old.
This idea of view models also comes from an answer Andy Matushak gave in the Q&A of the talk I linked above. He calls it view data, but I prefer to keep the word “model” in the name to make their similarity to model types explicit.
Coordinators manage the app’s navigation flow and dependency injection
Another good idea of MVVM and VIPER is to take the navigation responsibility out of view controllers. We do that through coordinators.
Given their central role, view controllers tent to know too much about the navigation structure of an app and other view controllers. But a view controller is already overloaded with managing its screen and dispatching events.
So we move anything that is not limited to a single view controller into a coordinator.
Unlike MVVM and VIPER though we don’t reject storyboards and segues. Define navigation using segues removes a lot of declarative code that would end in a coordinator. You use segues for the same reason you use interface files instead of writing UI in code.
When the user triggers a navigation event, a view controller receives the message in its prepare(for:sender:) method. After taking the appropriate decisions related to itself, it delegates navigation to a coordinator.
Since the coordinator sits at the center of the pattern and communicates with all view controllers, it’s also the perfect place for shared model controllers.
When navigation happens, the coordinator can inject the right dependencies into view controllers, including itself.
Without a coordinator, you can only pass shared resources from one view controller to the next, or use singletons. This causes view controllers to carry along dependencies they don’t need, to pass them to a view controller far down the line.
Interface controllers manage complex interfaces and custom transitions between view controllers
The last piece of the Lotus MVC pattern is interface controllers, an idea I see rarely discussed.
Again, this is not a new idea. I took it mainly from the section on customizing view controller transitions of Apple’s View Controller Programming Guide.
If you want to customize view controller transitions, you need additional objects. Namely, a transitioning delegate and an animator.
We can abstract this idea and decouple it from the implementation details. What we get are interface controllers, objects that encapsulate complex view code, removing one last responsibility from view controllers.
Since interface controllers deal with views, according to the Apple’s definition I quoted above they are also view controllers.
But view controllers in iOS are something more specific than they were in Mac development. Interface controllers are not subclasses of UIViewController and have different responsibilities, so I gave them a distinct name.
In the pattern diagram, interface controllers are also connected to the coordinator. Like everything in the pattern, that connection is not mandatory. It’s needed only for custom transitions between view controllers.
That’s not always the case. An interface controller can handle only the interface of a single view controller with complex logic or animations.
Often, developers subclass UIKit classes to add such logic, but composition is a better solution.
If you ever added a third-party library to your app that required you to subclass their particular UIKit subclasses breaking your inheritance structure, you know what I am talking about. I will provide a concrete example below.
the Lotus MVC Pattern in action
At first sight, the Lotus MVC Pattern might seem more complicated than other patterns.
There is a definite benefit in having more defined roles though. When writing code, you can focus only on subsets of components, instead of worrying about all.
In this section, I will outline how to apply the pattern to a real app.
Pick only what you need
It might seem that the Lotus MVC has many more moving parts than MVVM or VIPER. If you compare their diagram and count the actual roles though, you will find that there is not a substantial increase.
One of my goals was to avoid the prescriptive nature of other patterns. In MVVM you always need one view model for each view controller. In VIPER, you always need a presenter and an interactor.
In the Lotus MVC Pattern, you only implement the components that you need.
- You need interface controllers only for complex UIs or custom transitions. Most apps don’t have either.
- Simple views that do not require specialized data formatting don’t need view models either. Think of a login screen, where the username and password are plain strings.
- View controllers without table views or complex behavior don’t need dedicated model controllers.
This pattern also has another advantage: thanks to its well-defined roles, you can focus only on a specific subset of components when writing your code.
I got asked for examples of real apps where these concepts are applied. I know my students use it, although I don’t know the details of their jobs.
I also reached out to a previous client of mine to feature their app as a case study, but at the time of writing, I didn’t get any reply. So I will use the sample app for my iOS Architect course.
A complex networked app connecting to the GitHub API
In the course, I show the step-by-step creation of a client for GitHub. Despite being an example, it is still a full-fledged app, on par with real apps in the app store, including:
- full visual UI design;
- a substantial number of screens and complex navigation;
- static and dynamic table views;
- OAuth authentication;
- full-fledged networking architecture using the GET, POST, PUT, PATCH and DELETE methods of HTTP;
- secure storage of credentials in the iOS Keychain;
- caching of network requests;
- data persistence on disk;
- error handling;
- scheduling and canceling of concurrent network calls.
These are three of the many screens in the app:
I can’t explain everything that goes into that app since that requires a whole course. But I can show you the main architectural ideas and how to apply the Lotus MVC pattern in full.
Coordinating the OAuth login flow
Since the app requires authentication, it presents a login screen on the first launch. The app also presents this screen any time the OAuth token becomes invalid.
- In the OAuth login flow, the authorization happens through a web form. So this login screen does not sport text fields for username and password.
- Since this screen is straightforward, there is no need for any view model, interface controller or dedicated model controller.
- The OAuth protocol requires the app to send an authorization request to a token server. The login view controller uses a network controller for the task.
- Access tokens need to be securely persisted in the iOS Keychain. A keychain model controller takes care of this. The network controller manages the keychain controller, keeping authentication details hidden from view controllers.
- The network controller is injected into the login controller by the coordinator. This object also manages the presentation of the GitHub login web form using an SFAuthenticationSession instance.
Displaying the profile of any GitHub user with a table view
The app sports a profile screen that shows the information of the primary user of the app, as well as the profile of any other GitHub user.
- I implemented this using a dynamic table view. A model controller acts as the table view’s data source. As I explain in another article, pretty much any scrolling screen in iOS apps can be implemented using a table view.
- The data source has a copy of the Profile model type, which is a structure. This way it can break its data into rows for the table view. The view controller is relieved from the responsibility, and its code is significantly reduced.
- Cells require special data formatting, so every cell has a view model. In the diagram above I only show the SummaryCell for simplicity, but the app has a custom cell for every row in the table view. Not all of them have a view model though. The cell for the avatar does not need one since all it needs is a UIImage.
- The view controller fetches its data from the GitHub API, so it also has a reference to the network controller.
Notice that the data source has no connection to the network controller. In the general diagram for the Lotus MVC Pattern, there are no connections between dedicated model controllers are shared controllers either.
Any class that touches asynchronous code becomes asynchronous. If the data source had to retrieve its data from the network, we would have an asynchronous data source, which is harder to manage and test.
As I said, in this pattern, view controllers are the central piece coordinating all other objects. The view controller requests the profile data using the network controller and then passes it to the data source.
Elaborate table views with editable text fields
The user of the app can edit his profile and submit changes to the GitHub API.
- This screen also uses a table view, so a data source is again required. In this case, though, the data in the cells do not need special formatting, so there is no view model.
- We need to preserve the user’s input in the data source. Otherwise, we will lose it when the table view scrolls and reuses a cell for another row. Cells notify the view controller of updates through delegation. The view controller then updates the data source.
- A typical behavior in iOS forms is to move the cursor to the next text field when the user taps on the return key. This behavior is trickier when using a table view. You need to find the next cell with a text field, scroll the table view to make it visible and make the text field become the first responder.
- This is implemented through composition, using an interface controller instead of a UITableView subclass. This allows us to reuse the interface controller across projects, without disrupting their inheritance hierarchy.
Managing the app’s navigation and the injection of shared resources
There are separate navigation flows, and view controller communication involved even between just the three screens we are considering.
- Multiple storyboards and segues define the whole navigation flow. You can find an in-depth article about storyboards here.
- Since a coordinator manages the navigation of the entire app, its code can grow pretty quickly. For that reason, the app uses a hierarchy of coordinators instead of just one.
- View controller communication and the injection of the shared network controller happens through coordinators. In this way, view controllers don’t know anything about each other. They also get only the dependencies they need. For example, each view controller references just the required coordinator, instead of the whole set.
Since the app performs many different network calls, the networking architecture is more complicated than a single network controller might indicate.
As we have seen, the Lotus MVC Pattern is a comprehensive design pattern that addresses all the five aspects of iOS app architecture:
- It avoids massive view controllers and other monolithic classes by separating concerns across components with well-defined roles.
- It simplifies the app’s execution flow by using value types for its model and avoiding binding mechanisms like KVO or reactive frameworks. (You can use those with the pattern too if you want, but I don’t recommend it).
- It centralizes state sharing using shared model controllers and avoiding singletons.
- It implements view controller communication and dependency injection through coordinators. At the same time, it respects the iOS platform, keeping view controllers as a central component and integrating storyboards and segues.
- It addresses parallelism by isolating asynchronous code into a shared network controller. Only view controllers access the network controller, keeping all the rest of the code synchronous.
For these reasons, I think that this pattern is superior to MVVM and VIPER.
And also because, well, I created it. It would be strange if I didn’t.
If you want to see some of these ideas applied, I have a free mini-course which you can get in the form below.