At Mutual Mobile we believe that testing is part of building great software. However, testing has not always been a key part of building iOS apps. As we embarked on a quest to improve testing across many of the apps that we build, we found that writing tests for iOS apps was difficult. We decided that if we were going to improve the way we test our software that we would need to first come up with a better way to architect our apps, which we call VIPER.
The traditional way of architecting an iOS app is using MVC (Model View Controller). Using MVC as an application architecture can guide you to thinking every class is either a model, a view or a controller. Since much of the application logic does not belong in a model or view it usually ends up in the controller. This leads to an issue known as Massive View Controllers, where the view controllers end up doing too much. If all of the logic is embedded in the view controller it leads to testing the logic via the UI, which is the wrong way to test logic. It is also easy to mix business logic and UI code in the same method. When you need to add functionality in the future, or fix a bug, it is often difficult to determine where to make the change and be sure it will not have unintended consequences elsewhere.
In looking for a better way to architect an iOS app I ran across the Clean Architecture, as described by Uncle Bob. Clean Architecture divides an app’s logical structure into distinct layers of responsibility. This makes it easier to isolate dependencies (e.g. your database) and to test the interactions at the boundaries between layers.
VIPER is our application of Clean Architecture to iOS apps. The word VIPER is a backronym for View, Interactor, Presenter, Entity and Routing.
In short you have:
Initially coworkers were referring to this architecture as VIP architecture. In some ways this felt derogatory though, because that could stand for “Very Important Architecture”. Since I didn’t want people to infer that other architectures were not important I decided to call it VIPER, and came up with what the E and R meant later.
This separation also conforms to the Single Responsibility Principle. The Interactor is responsible to the business analyst, the Presenter is responsible to the interaction designer and the View is responsible to the visual designer.
Below is a diagram of the different components and how they are connected:
An Interactor represents a single use case in the app. It contains the business logic to manipulate model objects (Entities) to carry out a specific task. The work done in an Interactor is independent of any type of UI. The same Interactor could be used in an iOS app or a console application.
Because the Interactor is a PONSO (Plain Old NSObject) which primarily contains logic it is easy to develop using TDD.
Entities are the model objects manipulated by an Interactor. Entities are only manipulated by the Interactor. The Interactor never passes Entities to the presentation layer (i.e. Presenter).
The Data Store (e.g. web service, database) is responsible for providing Entities to an Interactor. As an Interactor applies its business logic it will typically retrieve Entities from the Data Store, manipulate the Entities and then put the updated Entities back in the Data Store. The Data Store manages the persistence of the Entities. Entities do not know about the Data Store, so Entities do not know how to persist themselves.
When using TDD to develop an Interactor it is possible to switch out the production Data Store with a test double/mock. Not talking to a remote server (for a web service) or touching the disk (for a database) allows your tests to be repeatable and faster.
The Presenter is a PONSO which mainly consists of logic to drive the UI. It gathers input from user interactions so it can send requests to an Interactor. The Presenter also receives results from an Interactor and converts the results into a form that is efficient to display in a View.
Entities are never passed from the Interactor to the Presenter. Instead, simple data structures that have no behavior are passed from the Interactor to the Presenter. This prevents any “real work” from being done in the Presenter. The Presenter can only prepare the data for display in the View.
The View is passive. It waits for the Presenter to give it content to display; it never asks the Presenter for data. Methods defined for a View (e.g. LoginView for a login screen) should allow a Presenter to communicate at a higher level of abstraction, expressed in terms of its content and not how that content is to be displayed. The Presenter does not know about the existence of UILabel, UIButton, etc. The Presenter only knows about the content it maintains and when it should be displayed. It is up to the View to determine how the content is displayed.
The View is an abstract interface, defined in Objective-C with a protocol. A UIViewController, or one of its subclasses, will implement the View protocol. For example, a login screen might have:
Routing handles navigation from one screen to another as defined in the wireframes created by an interaction designer. A Wireframe object in responsible for routing. The Wireframe object owns the UIWindow, UINavigationController, etc. It is responsible for creating an Interactor, Presenter and View/ViewController and installing the ViewController in the window. Since the Presenter contains the logic to react to user inputs, it is the Presenter that knows when to navigate to another screen. The Wireframe knows how to navigate. So, the Presenter is a client of the Wireframe.
You can find Counter, a simple app which shows the use of an Interactor, Presenter and View on GitHub. A future article will go into more detail of how that app was developed. Additional articles will illustrate the use of a data store and Wireframe.
In summary, VIPER helps us be more explicit about separation of concerns by splitting lots of code from one class into multiple, smaller classes. By maintaining a single responsibility in each class it will make it easier to develop those classes using TDD, which allows us to more quickly respond to changing requirements and build better software.