Advanced iOS Architecture: Solving the 5 Issues of the MVC, MVVM and VIPER patterns

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 presented it publicly for the first time at MobileFest in Kiev, and this is the first time I show it publicly in a detailed article.

This article revolves around UIKit apps, but most of the ideas still apply to SwiftUI, since the Model-View-Controller pattern remains essential.

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Contents


Section 1

The Problems of Popular iOS Design Patterns


Section 2

The Lotus MVC Pattern


Section 3

Case study: the Lotus MVC Pattern in action

Section 1:

The problems of
popular iOS design patterns

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. 

app follwing the classic MVC pattern

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.

lighter view controllers

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.

the classic MVC pattern diagram

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.

a vertical view of the MVC pattern

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.

view controller communication and code execution

View controllers need to pass data to each other when they come on screen when the user moves around an app.

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:

the network singleton in the MVC desing pattern

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;
  • parallelism.

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:

the classic MVVM design pattern diagram

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:

a vertical view of MVVM+C design pattern

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 Xcode storyboards, but I already wrote, in that article, 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:

the classic VIPER design pattern diagram

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:

a vertical view of the VIPER design pattern

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.
Section 2:

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 full Lotus MVC design pattern diagram

I have been teaching this new pattern for years to the students of my courses. This is the first time I write about it publicly, after recently presenting it a conference.

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:

view controllers are at the center of the Lotus MVC design pattern

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:

Controlling complexity in Swift

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.

the inert model in the Lotus MVC pattern

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.

You can compare their code to a pure function in functional programming. Even mutating methods, if you consider the old value to be the input and the new value to be the output.

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.

model controllers in the Lotus MVC pattern

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;
  • etc.

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:

view models in the lotus MVC pattern

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.

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.

coordinators in the Lotus MVC pattern

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.

interface controllers in the Lotus MVC pattern

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.

Section 3:

Case study:
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:

screens mockup for a sample iOS 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.

the app login screen

  • 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 UiTableView.
  • 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.

The app's screen to edit the user's profile

  • 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.

Wireframe of the app's navigation flow

  • 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 architecture for network requests is more complicated than a single network controller might indicate.

Conclusions

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.

Architecting SwiftUI apps with MVC and MVVM

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

37 thoughts on “Advanced iOS Architecture: Solving the 5 Issues of the MVC, MVVM and VIPER patterns”

  1. That’s awesome, thank you for your blog it’s trully an iOS information gold mine. However I would love to see the app (or another one) to study it directly in Xcode in order to understand how do you implement all of this

    Reply
    • Ah, I didn’t know about RIBs.

      I will have to look into it in detail to give a good opinion.

      From a very superficial look, I am skeptical. It looks rigid like VIPER, and I am not convinced of the idea of UI completely driven by the model, since it implies moving to the model navigation logic and state, which you get “for free” with view controllers.

      Reply
  2. Matteo, great post! You mention the following:
    “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.”

    If a segue is triggered via storyboard, doesn’t the segue just happen and that’s it? How can the coordinator play a role in presentation once the segue is about to start? Maybe I’m misunderstanding what you mean by “presentation”

    Reply
    • Good question.

      If it is just a matter of a segue happening, that is true. The coordinator is not needed. That is still ok from the point of view of this pattern, since navigation happens outside of view controllers.

      But often, there is more to a segue. Some examples:

      – passing data to the next view controller. This creates coupling between view controllers, especially when there are many navigation paths going forward. In this case, the view controller takes the context (for example, the row selected in a table view) and passes it to the coordinator. The view controller has no idea what happens next, it simply tells the coordinator: this row was selected. The ccoridnator knows the class of the next view controller and passes the context forward, along with dependecny injection of other resources.

      – conditional navigation. Sometimes a segue is not connected to a control (like a button), but to the view controller itself. Depending on other context, navigation can proceed along one path or another. For example, in a table view with different rows, you might go to different view controllers. In that case there is some decision to be taken before triggering a segue in code using the perform(_:sender:) method. That decision is again taken by the coordinator.

      Reply
    • Yes. The main coordinator is created by the app delegate when the app starts and kept in memory by it.

      I guess I should have added the app delegate to the diagram, now that I think about it.

      Reply
      • Thanks a lot. And where should the business logic that is not related to network go? Should this be done in another model controller(which holds network controller) that gets the models from network controller and processes it? And whats your opinion on notifications. Eg: UserLoggedIn notification. Can this be posted from model controllers and observed in view controllers?

        Reply
        • Your questions are a bit complex, so I can only give you some general guidelines in a comment.

          Regarding other logic, it might go into a separate model controller (like a data source). But as I stated in the article, dedicated model controllers are not connected to shared ones, like networking. This spreads asynchronous code and complexity. View controllers are the central objects that mediate between all others.

          Regarding notifications, I only use them when needed, which is very rarely. A login view controller can be presented by the main coordinator, on top of the initial view controller, which is always on screen. The login event then just needs to go back to the coordinator. There is no need for a notification.

          Reply
  3. I’m very much advocate of SOLID myself. But complexity and rigidness of a certain approach depends on skills and experience against blind following of Clean Code “recipes”.
    Like in VIPER. You can have multiple VCs within a module. You can even have more of them nested in one view. That doesn’t necessary mean each and every VC needs full monty pack of Interactors and Presenters. Some smaller views even wouldn’t have any view controllers, just their view models, who will be produced either by simple factories or main Presenters.
    There are definitely many ways, how to adhere to SOLID principles without getting too much rigid.

    Reply
    • Agreed. As I mentioned, there are different flavors. In some VIPER implementations not all objects are required, while in others they are.

      Still, I don’t like the roles of VIPER, where responsibilities are distributed too thin. You can change VIPER towards a more flexible approach, but then you would probably get something along the lines of what I present here, if not the same.

      Personally, I like to have a pattern where roles are more explicit intstaed of being subject to interpretations. But that’s just me.

      Reply
  4. I generally like the concept, because it’s clean, but few things bother me a bit:
    I see a lot of controlling and coordinating objects, smells like a lot of orchestration needed. This might bring some following issues:
    – loads of referencing, either explicit or through interfaces, in both cases a lot of memory handling, in first case also subject of potential too much coupling
    – some orchestrating objects potentially need to know too many objects and know too much in general, which seem like an overkill on a level of one physical view
    – doesn’t seem much simpler compared to other architectures/approaches mentioned and compared with, unless there are some boiler plate reusables, a lot of coding might be necessary just to set up the wireframe
    – less skilled devs might struggle with understanding and maintaining the concept.

    Perhaps there could be some sort of simplification in terms of more unilateral binding of the objects, some implicit routing with more strategy pattern. I’m not saying it’s possible, of course, but there might be the possibility.

    As said, I think the concept is good, because it does solve the basic problems of MVC with adhering to the basic principles of OOP.

    Reply
    • These are all valid concerns.

      I’ll try to address them as much as I can in a comment:

      – I try to keep referencing and coupling to a minimum. I indeed use interfaces (protocols), but that’s a prescription of the Dependency Inversion principle anyway.
      – Memory management proceeds in a top down manner, but it’s hard to explain in just one artcile. In practice, strong references follow tow paths (to simplify). Either app delegate -> coordinator -> shared model controller, or view controller -> dedicated model controller. This avoids strong cycles.

      – Coordinators (which is are orchestrating objects by definiton) definitely run the risk of knowing too much. I usually break them into more limited coordinators specialized on a single flow, like a login flow. The alternative is spreading these responsibilities across view controllers.

      – I knew it ran the risk of looking more complex than other patterns. Role wise, there are maybe 1-2 more than VIPER, but in term of object count it can definitely be higher. It depends if you see this as a problem or a feature. I prefer to have code separated in more well-defined modules, which unavoidably increases the number of types.

      – Yes, it’s not for less skilled developers. I put the word advanced in the title for a reason. It can be simplifed though, and I teach a simpler version of it in my beginner’s course, which no one has ever esxpressed complaints about. But I have seen beginners struggle with VIPER too, or with monolithic objects in general. I would say that less skilled developers will struggle in any significantly complex project, regardless of the architecture.

      In this older article I show a simplified version of what we could call “MVC + Model Controllers”, which is Apple’s original idea: https://matteomanferdini.com/model-view-controller-ios/

      Reply
  5. Hi, great post, thank you! Can you share the sample app implemented in this post? I really want to see it in action.

    Reply
  6. I’d love to read more about state machines for complex view controller logic. Do you plan to write a blog post about that topic?

    Reply
  7. Dear Matteo, thanks for a great talk,
    I currently use MVVM + C (RxFlow), the nicest thing about the coordinators for me is that it allows to use a dedicated Storyboard for each ViewController, which has a lot of advantages, specifically avoiding merge conflicts with segue loaded storyboards. How does Lotus approach this problem?

    Reply
    • In my opinion, using a storyboard for each view controller defeats the purpose of storyboards. I assume that you still use segues in each one of these storyboards, via storyboard references. But I don’t see much difference from using nib files instead.

      I do use multiple storyboards, but I don’t get to that level of granularity. I put separate, defined flows in each storyboard and that already reduces merge conflicts significantly.

      It is also my impression that if you merge changes directy in Xcode, merge conflicts in storyboards are significantly reduced. I wrote more about that here: https://matteomanferdini.com/ios-storyboards-xcode/

      Regarding coordinators, the way I use them they are completely independent from storyboards / nib files / view controllers in code.

      Every view controller gets a reference to a coordinator through dependency injection, same as other dependencies. They do reflect the structure of my storyboards though, usually. So if I have a storyboard for a user flow, I probably have a corresponding coordinator.

      Reply
  8. This is definitely the best architecture I have seen by now!

    Is it possible to share the source code of this application you’ve built for the GitHub API?

    I don’t mind paying just to be able to have access to the code.

    Reply
  9. Love the idea of a more “fluid” architecture, and one with VCs and storyboards at the center.

    In fact, I’ve been partly applying your lotus architecture to two projects now.

    There is one thing for which I still haven’t found a clear description yet, though. How do you integrate segues in your nav coordinators ?

    Almost none of the coordinator pattern implementations I’ve seen deals with segues, and practically all of them only use storyboards as a kind of glorified nibs.

    Even a small example of how you would use segues in a coordinator would go a long way to help.

    With regards to multiple coordinator, do you usually have a parent-children relationship within them (such as, for example, a topCoordinator, that instantiates a number of specialised coordinator, such as loginCoordinator, checkoutCoordinator , and so on), or do you use a number of separated ones ?

    Reply
    • I never understood developers that use storyboards as nib files without segues. It gives them only the drawbacks of a storyboard and none of the benefits.

      Regarding segues in coordinators: you can’t get a way from the fact that prepare(for:sender) is called in view controllers, so that piece remains there.

      What I do is move all the code that performs dependency injection in a method of the coordinator, and call that method from prepare(for:sender).

      For those cases in which navigation is conditional, again, I move the conditional code to the coordinator, which is the object that calls performSegue(withIdentifier:sender) on the view controller.

      This way, the only objects that reflect the structural information in the storyboards are coordinators.

      For multiple coordinators I do use parent-child relationships, but I keep the tree flat. So I usually have one main coordinator for the whole app and child coordinators for each separate subflow. Otherwise managing coordinators at the “crossroads” in the storyboard where they meet becomes too complicated.

      Reply
  10. Very comprehensive comparisons!

    I’m rather pedantic and noticed the vertical stacks of MVVM diagram has the wrong directions between ViewController and ViewModel, but otherwise great.

    Reply

Leave a Comment