iOS: Principles of VIPER
I know, I have already written several articles regarding VIPER and other clean architectures, including which one is the best that fits your project, but since my first article was written 3 years ago, with all the grown knowledge I got across these years, it's time to write in deep details about all the aspects of VIPER that I have to provide you. Said that, let's start.
But we have already MVVM!
It's a fact that MVVM is a great solution for project's scalability, reusability, readability and testable purposes, but there are some reasons why it may not be enough depending on the project.
The first thing I would like to highlight here and most people are not going to agree with is that MVC and MVVM are not exactly architectures, but design patterns. The VIPER architecture actually englobes all of the MVVM concepts and only add some new aspects regarding the SOLID principles. Here is what MVVM truly is:
If you read my very first article, I may have said something like the Model layer is just some fixed place where we can place some business logic and it shall communicate with the ViewModel in order to trigger some updates in our presentation layer. Fact is that Model
is much more abstract than I thought and it definitely is not just a class, but a concept. We may rely our Model
"layer" in a set of business classes or some Calculator
structs that do specific tasks to us. The Model
can even be a service where we can retrieve/update data. The idea of the Model is that it is some place else where the ViewModel
may rely its inputs and outputs.
And VIPER has exactly everything MVVM does, but it describes deeply what the Model is by creating more fixed layers. In generic MVVM, Model classes may have a lot different names you might be familiar with when you worked on some project: Calculators, Parsers, Service, Repository, Facade, Domain and so on. That's the core difference between architectures and design patterns: Architectures have fixed layers with specific names and a single purpose, design patterns may solve problems in different scenes and may not be present in every place, only when necessary.
Enough talking, let's take a look at our major template:
VIPER layers definitions
Firstly, I want to be very clear that VIPER's nomenclature stands from View-Interactor-Presenter-Entity-Router, which are exactly its fixed layers names.
The Entity
rectangle is a bit smaller than the others because, surprisingly, it's not a standard layer as the others, although other people may think I am wrong.
Let me introduce the definition regarding each one:
- View: It consists of our UI layer, UIViews and UIViewController subclasses establishing some hierarchy. No logic(conditional statements, loops, parsing) should be kept in there. All the outputs come from our ViewModel, oops! They come from our
Presenter
. - Presenter: It may correspond exactly to what the
ViewModel
does in the MVVM: presentation logic. Everything should be placed in there, deciding which is the context of our screen, what important data should be presented and the content should be kept in a UI-friendly language that is ready to fill our Views, without any parsing. Besides,Presenter
must answer to all user interactions and decide when it's time to move to another scene and trigger theRouter
. The business rules that used to be implemented by ourViewModel
in MVVM now moved to theInteractor
.
There is a discussing about the Presenter
being equivalent to MVVM's ViewModel or MVP's Presenter. The answer is: both. It's up to you, if you want more reactive data to fill our UI on demand, implementing as an MVVM ViewModel
is the best choice. If you want this layer to tell our View every time an update is needed, MVP's Presenter is the way.
3. Interactor: This layer corresponds to our use cases within the screen. Basically, every time we want to execute some logic inside our business domain, we should trigger some method that does that in our Interactor
and returns some output. This class is also responsible of being a hub to data sources like API calls and data persistence.
4. Entity: I don't like to call that a layer because there is actually no abstraction inside an entity. Remember Entity-Relation diagrams from Peter Chen you learned in your Data Modeling course? It is just pure entities. It's just a struct/enum describing some entity inside our domain. No logic is needed, and usually they implement the Codable
protocol.
5. Router: It's basically the class that is responsible for the Coordinator
design pattern, same meaning. It should keep a reference to our UINavigationController
(or the viewController itself) in order to perform push, pop and present operations and do a change of context. Besides, some people like to inject the navigation into ourRouter
from Presenter
instead of being a property. Most companies like to call this layer as Wireframe
, personally I prefer Router
, more declarative.
For short, our procedure is that View
sends user interactions to our Presenter
, which may or not trigger some use cases in the Interactor
or just change or presentation rules without any business logic. The Interactor
performs some use case operations relying on entities and send back the results to the Presenter
, which change the presentation layers and force an update in our View.
Communication between the layers
Most people defend delegation as the exclusive way to establish communication between the layers, but I strongly disagree. You can perfectly define closures to create a callback to handle the outputs. You can rely on reactive approaches to handle each outcome such as RxSwift, Reactive Swift and Combine, which I prefer if I am working at a declarative project. The important thing to mention is that everything, except by the View, should be unit testable. All upper layers must be representable as a mock and the lower levels as a Spy. For that, each type shall have an interface(protocol)
For me, Combine is the most suitable protocol of communication for VIPER, mainly if you adopt SwiftUI as your interface.
Implementation: Search
Enough talking, let's code. The project we are about to implement doesn't truly rely on any API or external source in order to decrease complexity. Instead, we are creating our data models through some mocks and using that to fill our interface. This project is a simple contact list with three major use cases that we shall cover during this tutorial:
- Fetch contacts
- Search for a contact
- Fetch contact details
We are about to fetch a list of contacts with just three informations:
- Image
- Name
- Number
This is the result we are expecting:
There are two scenes that hold VIPER layers: Contact List and Contact Details, which may also present an error view. We will cover each of them layer by layer, starting by the Interactor:
1. Interactor
It takes care of business rules. All the logical operations regarding data models and API calls triggering should be handled here. The most important aspect about the Interactor is that it doesn't know anything about the screen, UI and UIKit at all. Only logical operations happen here and it should only send the outputs to the Presenter
.
There are two use cases that may occur in our contact list: fetch the items and filter an item. Each of them brings some output to be reflected in our UI. Check the code:
As you can see above, we are defining two different protocols, one for the Interactor
input, simply consisting of our use cases and the other to send the outputs, which shall be our lower layer, the Presenter
.
Now, let's implement our business rules! First of all, we rely on some data model to represent our contact in the scope of this screen:
This is our entity regarding the contact list logic. You can see we predefined some values that shall fill our list. Basically, the Interactor
is responsible for providing the data in order to have our entities, may it come from an API, a database, local persistence, mocks or whatever. In this case, aiming a decrease of complexity, we are instantiating the values.
Now we can implement our concrete Interactor
:
We implemented both use cases:
- We assign our
ContactListEntity
stub to ourcontacts
memory and them we send it backwards to ourPresenter
- We take a prefix and search for all the contacts in our entity memory that match. Then, we send it backwards in the same way.
Cool, these are our use cases for the contact list.
2 . Presenter
The Presenter is the layer responsible for parsing our use cases outputs into a suitable format in order to fill our UI. Basically it deals with string formatting, UI event handling, saves constants and knows whenever it's time for a change of scene. It's the central layer, so I would say it's the screen's core.
Talking about our contact list scene, it's responsible for:
- Presenting our items list in a UI friendly way
- Listen to UI lifecycle and interaction events
- Decide when to show the details of a contact
Differently from our Interactor
, the Presenter
is allowed to import UIKit. Besides, the Presenter
shall implement the ContactListInteractorOutput
protocol which the Interactor
sees the Presenter
through.
There are two protocols defining our Presenter
contract: an input protocol defining the possible events that are triggered by the View
and another to trigger an update operation in our View
. Since the View
doesn't have any kind of logic(a.k.a if then else and loops), the Presenter
deals with all of it regarding our UI.
Since the Presenter
converts our entities into a UI friendly format, there is another type that I should mention. The ViewModel
, differently from it's MVVM meaning, is kind of an entity that may fill our UI:
It's the same of our ContactListEntity
, but now we see our image as an UIImage
instead of a pure string. This is the type that our UI is prepared to receive and consume.
Now let's implement our Presenter
:
As you can see, the Presenter
has a reference to each of the other layers: View
, Interactor
and Router
. It fetches the contacts from the Interactor
when receives a viewWillAppear
event from the View
and searches from some contacts when the user types in a search bar. Besides, it works as a delegate/datasource to the tableView, being responsible for defining its structure and content.
It's responsible of defining what is the content of our tableView and handling its interactions. Each cell is created according to our viewModel for that index. When a cell is selected, we trigger our Router
in order to send data to the next scene.
Our Presenter receives and handle our use cases outputs from the Interactor:
There are different literatures discussing if Presenter
is the appropriate place for implementing tableView protocols since it may also be done in the View
. In my opinion, it may trigger some logic and verification about which cell type should be instantiated and this way Presenter
is the proper place.
You may also found the Presenter
familiar with ViewModel
from MVVM, but that is the idea. It listens to all events in our View
and generates content ready to fill our screen. This way, the only event that our Presenter
sends to View
is updateUI
, which makes our ViewController
pick the content hold by Presenter
and update the screen.
3. View
View is the most dumb layer of our architecture. It consists of our pair ViewController/View and is responsible for handling our ViewController lifecycle and filling our UI with Presenter
content already generated by our presentation logic. It doesn't contain any logic, no conditionals nor loops:
As you can see, it only builds our interface, creates hierarchy, set up constraints, handles lifecycle events and fills content. Besides, it implements components delegates and Presenter
outputs:
When we are told to update UI, we reload data at our tableView. When our search bar changes its content, we should tell our Presenter
.
4 . Router
The router is responsible for changing our context. It should only have access to the viewController in order to push a new one:
As you can see, we have a protocol which our Presenter
can see and an enum defining which is our new context with our input data as associated type. We may cover the Factory type later, but it's important to know the ContactDetailsFactory
build method returns a view controller.
Contact Details
Now that we implemented our first scene for the contact list, let's conclude our project with our details screen, which is displayed just after selecting a cell from the list. It should contain all the other layers we just discussed, except by the Router
, and we will talk about why.
1. Interactor
We just have one use case for our Interactor: fetch details based on the contact name. Since we are mocking everything, we created a Hash structure to return an entity instance for each name:
If we don't get a contact matching the name prefix, it returns an error.
2 . Presenter
Now our second Presenter consists of two details that impact our UI: a details ViewModel, which provides the info to fill our labels and imageView and a boolean indicating if an error happened, and if it's true, we shall make an ErrorView
visible, which is instantiated by our Presenter
as well. The Presenter keeps some String constants defining the text for the error view.
Also, we have a method to handle the viewWillAppear
event. As we previously implemented, the Presenter
output has also an updateUI
method to "redraw" our scene.
We have a ViewModel type which describes our entity in a way it's ready to fill our screen:
The difference here is also in the image that we now represent as an UIImage
instead of a String
.
Now let's implement our Presenter:
As you can see, now our Presenter holds some constant data that shall fill our screen as well as some formats. The class receives as an input a name string from the previous scene and answer some UI events. Everything that dictates our content is safe in the Presenter
.
You also notice that the content our screen is about to display is resumed by our details
viewModel as well as our hasError
boolean, which tells us if we should present an error view. The error view is also instantiated by our Presenter. Both of them have an observer that tells our UI to be updated when value changes. The implementation of updateUI
is responsible of filling the new content. This is triggered when receiving new data from the Interactor use cases:
3 . View
As well as our ContactListViewController
, this View doesn't contain any block of logic, only lifecycle methods and content filling.
Here we also have some UI elements and methods to build our view code interface. As you can see, the setupContent
method is responsible to fill our layout with dynamic data that comes from the Presenter and is called every time we update.
4. Router(?)
As our details screen doesn't navigate to any other scene, it doesn't require any routing logic, so we are not about to create a Router class for our details scene.
Tests
As we have multiple layers, some concepts should be clear when testing each functionality:
- Only logic layers are about to be tested since our View doesn't contain any logic.
- Each suit under test(SUT) shall have a mock in order to avoid other classes to interfere in our testing subjects. It means when we test Interactor, services, core data and other external sources(including feature flags) should be mocked and return crude data. The lower layers that receive the test outputs shall work as a spy which verify if our output is correct.
In a few words, I would say that iOS skills inside corporative environments recommend testing Interactor and Presenter, and Router sometimes.
Other layers?
Not always we rely on business logic that is restrict to a single scene. Because of that, in some companies, VIPER contains a sixth layer we call Domain
. It works as an extra layer that contains business logic that may be reused across different scenes.
Some corporations also like to consider Service
as a VIPER layer due to the fact it builds some API calls based on a scene's context.
This classes are not mandatory to a VIPER project and I will not illustrate them, but it's very important to know these variations.
Conclusion
In this article we gave a deep explanation about the main concepts around VIPER and how to organize each kind of logic inside some screen. We differentiated the ways some companies see our VIPER architecture and how to provide relevant unit tests.
If you wanna check the entire code, this is the repository:
I hope you enjoyed and keep applying VIPER in your new projects;)
Wanna learn more?