iOS Architectures Explained: Which One Best Fits My Project?

And know when to use MVC, MVVM, VIPER, and Coordinator patterns

Pedro Alvarez
Better Programming

--

Source: Undraw

When you are planning to build an app, one of the most important decisions(if not the most important one) is to choose how to structure your app's core.

Better saying, you need to decide which architecture you should adopt to build all your screens, features, and contexts. You need to know how to organize your app in order to make it maintainable for the long term, testable, scalable, and understandable by anyone who enters the project sometime later.

Benefits of a good architecture

  • Establish a clean language to understand where is each kind of responsibility inside your features.
  • Make each responsibility separate from others to allow testing more easily and increase the comprehension of the scene.
  • Make your project maintainable by long term
  • Decrease coupling between the classes and components.
  • Fill in your project complexity expectations
  • Separate UI from business rules(mandatory)

Now that you understand that every mobile project needs a good architecture, you must know something. People ask me all the time which architecture is the best and which design patterns should they avoid.

For that question, there is a very simple answer: There is no best choice.

This question depends on a lot of variables like your project's type of business, its complexity, how you are going to organize your modules and where is it going sometime later.

Since there is no exact answer to this question, I will illustrate in this article all the most known architectures for iOS and which kind of project it attends and its main issues. I hope you enjoy it!

1. Model-View-Controller(MVC)

This architecture is the most standard one in the development world since it's used in many languages like Java, Swift, and Python. It's also the default pattern used in Apple projects since we have already some native classes relying on its layers.

It consists of three core layers as the name says: Model, View and Controller. Let's cover what each one is responsible for:

  1. Model: It englobes all the business rules for a scene. All data models and its operations and use cases must be kept inside this layer. It mostly saves the logic state of the screen(ViewController)
  2. View: It consists of a UI component holding multiple subviews whose hierarchy and states change according to the business logic operations output. You can interpret this layer as the Xib files, storyboards, which hold some UI hierarchy, constraints, and properties. UIView subclasses also belong to the View layer.

Pay attention that the first two layers implement respectively business and presentation operations, but how to link them since they are independent?

3. Controller: This layer receives the outputs from both business and UI worlds and passes those to update the other layer. Basically, when it detects some user interaction through an IBAction, it transmits the message to the Model to update the logic. When some result is achieved in the Model, it must update the view according to its state

Take a look at the diagram:

By default, Apple already provides us this architecture with Swift native classes(UIViewController and UIView)

Problems with MVC

  • Most people place a lot of business and presentation rules inside the ViewController making it massive.
  • ViewController is responsible for various types of tasks like servicing, listening to view interactions, implementing delegates and navigation logic
  • Hard to test
  • Really not simple to debug
  • Even messier when establishing children view controllers

When to use it?

  • When you have a very simple project like some iOS feature demo or testing an app feature in a Sample template
  • When you have trivial business rules

2. Model-View-ViewModel(MVVM)

This architecture came to solve the MVM main issues by decoupling some of its scene's responsibilities into a new layer: the ViewModel. Now your Model layer hasn't any logic because it consists only of simple data, it's usually a struct.

Take a look:

  1. View: Now it englobes both View and ViewController from the MVC. It's only a place to make the UI update operations and layout.
  2. Model: Just crude data, there is no function in this layer, just kind of memory for the state of the objects.
  3. ViewModel: Responsible for both business and presentation rules. It's where you place all the variables which you are going to read from the ViewController to update the UI. You can think of those properties as the screen's state. It also communicated with an API service if necessary

Problems with MVVM

  • Multiple responsibilities for the ViewModel
  • Multiple responsibilities for the ViewController
  • There isn't still any navigation layer
  • Still not easy to test(Just the ViewModel?)

When to use it?

  • When your project has some simple business logic that demands some testing
  • Not too complex use cases
  • Not a huge number of screens
  • The app is still in its concept phase, where you don't know exactly what it will be like in a distant future

3. Coordinator Pattern

Despite not being an architecture but a design pattern, I cannot ignore this solution since solves most of the problems in coupled architectures like MVC and MVVM.

Remember I mentioned some topics ago there was no layer to make a transition between two screens? This is exactly what the Coordinator is for.

Basically, it maintains a reference to the scene's view controller and through it, it can push a new one to its navigation controller(All view controllers can hold a navigation controller as a Swift optional) or maybe present a new screen modally. Some people even like to display custom alerts and other subcomponents through the coordinator.

The coordinator must access the ViewController to be able to change context

When to use it?

  • Any time you want to separate navigation logic from the rest of the scene

4. VIPER

VIPER comes as a definitive solution for architecture's coupling. This time you can separate all types of logic in each class. This way you can make totally independent tests, build mocks, spies and detect bugs much more easily.

The VIPER abbreviation stands for 5 different layers:

  1. Interactor: All the business rules are places here, since data model handling operations to API calls or error handling. It's based on use cases for better saying. It's important to mention that this layer is totally UIKit independent, which means it only deals with business logic, not knowing anything about the interface.
  2. Presenter: It's responsible for the presentation logic. It's always listening to user interactions in the ViewController handling(if needed) events like viewWillAppear , viewDidLoad , IBActions , and sometimes implementing some data source protocols(like UITableView's and UICollectionView's) or other UI components delegates. It is also responsible for receiving the Interactor's use cases outputs in order to translate it to a UI structure to be shown in the View. The Presenter also decides when it's time for a change of context.
  3. Router(or Wireframe): This layer has the same idea as the Coordinator design pattern, but now is a mandatory layer included in the VIPER architecture. This class has access to the context ViewController or Navigation controller, and this way it can change it to another screen by pushing a new UIViewController instance to the stack or presenting it modally. It's important to highlight that a Router structure must be able to manage the screen context in multiple ways. Usually, there is a Router superclass that implements various operations for changing a simple context
  4. View: This layer is the ViewController together with the UIView(or xib/storyboard). There is no logic in here making it totally dumb. The View only receives orders from the Presenter to render some content based on a ViewModel data structure. Always avoid if/else or loop statements in this layer.
  5. Entity: This consists only in a data model related to the screen's use cases. Only the Interactor has access to the entities. Usually, as they are simple data models, they are always structs, as value types.

We even have the service layer, which in my opinion should not be coupled with a scene because a different scene can reuse the same endpoint for another use case. The Service is more related to the back-end than the front-end use cases, but it's a topic for another article.

Note that all the communication inside the layers flow is bidirectional. The View notifies the Presenter with some user interaction events, which may trigger a use case in the Interactor, making or not an API request, returning some output to the Presenter, which can display in the View following some UI rules.

The Presenter also decides when it's time for a change of screen through the Router. It's also important to know that not all time the Presenter will call the Interactor for a logic use case, since an interaction can only trigger a simple change of screen or change some data purely in the presentation rules.

Problems with VIPER

  • It doesn't really attend a very simple project, since its main advantage is the business logic handling. In a small project, the Interactor may be almost empty every time and you may have just a few navigation rules to need a Router.
  • Some people have some difficulty distinguishing what is a business rule, what is presentation rule and how to correctly separate the layers.
  • As the Presenter is totally centralized and may deal with different tasks not needing to call the Interactor every time, the Present might be a little bit coupled.

When to use it?

  • You have a big project which relies on a lot of complex business rules and has a lot of screens or modules
  • Not all the interactions in the UI trigger a use case and so in this cases just the Presenter may attend that(not discarding that business logic still exists)
  • You have different formats for data models: an object for the API fetched data(Decodable), one for manipulating business rules in the Interactor and another for presenting in the UI(ViewModel)
  • Your team aims for a good testing framework making all the layers and responsibilities totally independent

5. VIP (Clean Swift)

A lot of people knows this architecture as the best one for all kinds of project, which I strongly disagree with. In fact, it resolves the VIPER problem with a strongly coupled Presenter, but it brings other side-effects which I will cover here.

Let's start by defining the differences between this architecture to VIPER. As VIPER, this abbreviation also stands for View-Interactor-Presenter, but this time there is no bidirectional flow, it's a cycle:

As you can see, the core layers are the same as VIPER, and this time we have a different flow: Every time the ViewController detects some user interaction it may notify the Interactor to start some business rules operations, and when output is achieved, it's sent to the Presenter and it may convert this output to a user-friendly format to be displayed in the View one more time. Always this way.

But, wait, where is the Coordinator/Router? Which design patterns should I user? It's not specified. In fact, I didn't describe the whole architecture structure on purpose. As I mentioned at the beginning, each company/team may have a different way of applying an architecture, it's not written in rock. In my own experience, I have worked with VIPs in two different ways, each one placing the Coordinator in a different place. Both rely on the way we pass data to another context. We will cover them:

5.1 Data Store

The first pattern for VIP is the Data Store. This architecture places the coordinator together with the ViewController and the advantage for this is that if we want to change a context we don't need to pass by the Interactor and Presenter to make a transition.

But, wait, if we need to pass some data to the next screen, how can we do that since the Interactor is not known by the Coordinator? That's where we enter with the data store concept.

What happens here is that the Interactor can be seen in two forms through protocols: Business Logic, seen by the View which triggers the business logic, and Data Store, which is a protocol that holds the variables which we may pass to another context or receive from the previous.

You can see the Interactor in this way as a container made for passing and receiving data in different formats from and to other scenes. So, if I want to make a transition to another screen passing some of my Interactor properties, I just need my View to call the Coordinator, the Coordinator takes the necessary data from the Interactor(DataStore) and instantiates the next screen passing that data to its own Data Store:

5.2 Presenter-Coordinator

This pattern you may be already familiar with. This consists on coupling the Coordinator with the Presenter and the flow now stands for View-Interactor-Presenter-Coordinator. As you pass through the Interactor, which holds the logic and data, you don't need a protocol to allow the Coordinator to access the Interactor, as it comes with the flow to the Presenter and Coordinator.

The only disadvantage of this pattern is that you always force the use cases to follow the same flow, even if we don't execute any business rule only changing screens:

VIP problems

  • As with VIPER, it's not applicable to small projects as it's not necessary to divide such simple logic.
  • You must follow the same flow every time even if not necessary if you are decided to use the VIP-C.
  • You too many relationships between the layers if you decided to use Data Store.

When to use

  • You have a huge project with very complex business rules and a lot of use cases.
  • When you must trigger your business rules almost every time when you trigger some user interactions

Design patterns

To have a strong, reusable, scalable, and testable architecture, the good use of design patterns is always welcome, and combining a lot of them can make your way of coding viable and understandable through the years. Thinking about all architectures no matter clean or not, let's take a look at the main design patterns you should desire when structuring your scenes:

  1. Factory: This pattern as the name says stands for a way of building and linking a set of objects relying on each other. It's a very common solution for instantiating each layer class and assigning their delegates and other dependencies. The Factory design pattern is also a good choice when you are working with heterogeneous table views.
All layers being instantiated according and linked according to the chosen architecture

2. Facade: This pattern stands for a container that reunites multiple dependencies in one place. This way, you may have access to all important resources from any place in your app. Also, you can use this in a way of saving multiple related objects in a single class to build a strong ecosystem and facilitating the application of business rules related to that

3. Adapter: As you have seen, each layer of the architecture sees the data models in a different way. So, when you need to convert some model to a view model, that would be nice to instantiate the view model with the model object assigning each property related to the previous object

4. Coordinator: We already described this pattern before, but just resuming: This is supposed to hold your screen UI context in a way of allowing a transition.

5. Dependency Injection: This pattern allows us to have each layer have the other ones as property and you can inject an object of that other layer type through the initializer. Each layer references the other as a protocol type, which allows us to not expose the concrete implementation of that type, only its contract. It's a good choice to provide a default initializer passing the concrete type to avoid writing it each time we instantiate that class:

6. Dependency Inversion: This pattern is directly related to the Dependency Injection. Since the layers communicate with each other through protocols, you don't have to necessarily inject the exact concrete type of that other layer.

If for example, you are testing your Interactor or your ViewModel, you can inject a mocked Service where you may define which result you are expecting in order to avoid your Service interfering in the logic you are actually testing

Conclusion

In this article, we saw a deeper overview of the main architectures that are used in most iOS projects in tech companies.

We described the main pros and cons for each one, but generally, if I could give you a message, I would resume this content by saying: If you are just creating an application to test a new feature or just a Sample, MVC is the best choice for you since you are only testing a separated iOS concept(or maybe a Swift Playground could work for you).

If you have an app that has a very small domain and it's not going to become too complex in the coming years, yet if you need to robustly test all your features, MVVM shall be a good choice.

If you have plenty of screens and need some practical and reusable way of changing your contexts, think about applying the Coordinator pattern.

Now, if you have a huge project where you want to test all single line of code within the business and presentation logic, you think about the long term and your app is about to grow a lot, it's essential to structure a good clean architecture(VIPER or VIP). This is the content I wanted to pass you on.

I hope you enjoyed it.

--

--

iOS | Android Developer - WWDC19 scholarship winner- Blockchain enthusiast