Clean Architecture For iOS Development Using The VIPER Pattern
When starting an iOS project, among the first obstacles developers will focus on besides the purpose of the application or what Cocoapods they’ll need will be how they will organize code, and maybe which design patterns will be followed. While most developers will stick to the tried and true MVC (Model-View-Controller) or MVVM (Model-View-View Model), there is a clever pattern called VIPER that many do not know about. VIPER may change the way you’re used to developing for the iOS platform, and like most things, it has positives and negatives.
The Usual Suspect
When first starting with iOS development, developers will hear a lot about MVC or Model-View-Controller. This Apple approved architectural pattern appears everywhere including Apple’s UIKit, most tutorial example apps, and a majority of the apps on the App Store today. Just like the name suggests, MVC is broken down into three responsibilities:
- Model: The application’s data and logic that manipulates that data.
- View: The User Interface that the user manipulates.
- Controller: Controls the logic between the Model and the View.
MVC will work just fine if you’re working on your first app or working in a small team, but as the app grows, developers begin to jokingly refer to MVC as “Massive-View-Controller.” As time goes on more and more logic gets shoved into the Controller, and the Controller becomes more bloated and untestable. This is what the VIPER pattern tries to solve.
What is VIPER?
The VIPER pattern is a Clean Architecture that conforms to the Single Responsibility Principle. VIPER strives to divide the app’s logic into distinct layers of responsibility. Going a couple steps further than MVC, VIPER is broken down into five responsibilities:
- View: Displays information from the Presenter and sends user interactions back to the Presenter.
- Interactor: Retrieves Entities and contains the business logic for a particular use case. They are view agnostic and can be consumed by one or many Presenters.
- Presenter: Handles preparing content for the display and intercepting user interactions.
- Entity: Simple data model objects.
- Router: Handles navigation logic for which screens should appear and when.
When implementing VIPER, each feature or module will follow the above structure. Since the app’s logic will be separated into smaller components, the views now become lighter and the logic now becomes more testable.
The Flow of VIPER
The basic flow of VIPER is fairly straightforward. The user is brought to a new View by the Router, the View notifies the Presenter that it needs data, the Presenter asks the Interactor for data, the Interactor retrieves the Entities (from network request or local database), the Interactor sends the Entities to the Presenter, the Presenter creates View Models from the Entities, the Presenter sends the View Models to the View, and the View displays the necessary data to the user.
To demonstrate creating a module in the VIPER pattern, let’s pretend we are creating an app that displays a table of cars. Each cell displays the car’s make and model. The user will have the ability to tap on a cell and see the details of the car, or they can click on a “Create new car” button to add a new car to the list. When implementing a new module, I find it easier to work bottom-up, so we will start with defining our Entities.
Since the app is dealing with cars, let’s create a simple struct object that will hold some basic information: the
car object is what our API service will return to us. It contains basic information like id, make, model, and trim of the car. However, when we want to display information about cars, we do not need to include all of their information since the table cells only need to display the make and model of the car. So, we can create a quick view model to represent just the make and the model of the car.
This quick view model will be created in the Presenter and passed back to the View.
Now that our Entities are established, let’s create our business logic or “use cases” for them. Our table view will need to be populated with cars from an API service. So we will create an Interactor that handles retrieving cars from the API and sending them to the Presenter.
To do this, we declare a protocol method called
getCars that will use our API service to get the cars and return them. Since our app is pretty simple, we do not need additional use cases (although most real applications have several use cases).
With the Interactor in place, we now have a way to retrieve cars that we will eventually display. As stated earlier, the Presenter is in charge of reacting to a user’s input and preparing content for the display. Our app’s description mentioned the need to display cars (make and model), the ability to show a car’s details, and the ability to create and add a new car to the table. Next, we will create a Presenter that allows us to do just that.
For our ability to show cars, we have added a
showCars method that uses our previously created Interactor to retrieve the cars and then create simple View Models from those objects that will be used for specific types of cells within the View. The next two methods,
showCreateCarScreen, will use the Router (this will be created next) to navigate the user to the correct screen. These are three methods that our View will use to enable functionality.
What’s an app without navigation? The next step will be to create a Router that handles navigating a user to different screens depending on the action. So far, our app requires us to navigate to a car detail screen and a create new car screen. Let’s implement those two scenarios now.
Our two methods
showCreateCarScreen simply instantiate new View Controllers and use a presenting View Controller’s Navigation Controller to push them onto the view stack.
Now that we have a way to retrieve cars for display and several means for navigating the user to different screens, our next step is to create a View Controller that relies on the Presenter to handle its content and actions.
The first thing we may notice is our View Controller is now extremely light. There is no logic done in our View Controller, just simply calls to the Presenter which delegates what should happen. No more Massive-View-Controller!
Testing With VIPER
Due to the VIPER pattern’s distinct layers of responsibility, unit testing can be accomplished very easily. Each level can be tested individually without the need to set up complex environments. To demonstrate this, we can test our
CarPresenter. This Presenter handles loading car data, showing the car detail screen, and showing the create new car screen. Let’s create some tests.
Since we created our Router and Interactor methods with Protocols, we can easily mock those classes for testing purposes. The
CarPresenter is then created using those test classes, and within the tests we can easily verify that our expected behavior is correct.
The Benefits of VIPER
In the seven months that I used the VIPER pattern, I identified some advantages:
- Ease of testing: Due to the loosely coupled nature of the VIPER pattern, adding unit tests to every object is simple compared to other patterns.
- Handles large teams: The project that I used VIPER on had roughly 15 developers. Because of the separation of modules within the pattern, developers can easily work on different parts of the app without stepping on each other’s toes. Using VIPER also helps to reduce merge conflicts.
- Ease of development: Once the project is fully set up on VIPER, it becomes easier to add a new feature to a module. If you need a new way to sort something, you can add a new method in the Presenter. Developers can couple the VIPER pattern with Dependency Injection to add new features quickly and easily.
The Negatives of VIPER
While the VIPER pattern might help eliminate Massive-View-Controllers, it does come at a price.
- Too much boilerplate code: As you can see in the examples above, a lot of template code needs to be created to create a simple application. This can slow down development in the beginning and add unneeded complexity. The increase of files can also add to the compile time.
- High learning curve: Due to the increase complexity of VIPER over MVC, the team might need more discussion and training on to properly implement the pattern across their application.
- Full buy-in: In my experience, the whole project needs to use the VIPER pattern. VIPER does not play nicely with Massive-View-Controllers, partially implemented VIPER modules, third party Cocoapods that do not use VIPER, and even Apple’s own frameworks. When using VIPER with existing code or new frameworks, extra work is required to ensure code compatibility.
Although the world of iOS development is tightly coupled to MVC, the benefits of using the VIPER pattern in iOS development can be transformational for the right kinds of projects and teams. I’d recommend evaluating the VIPER pattern for larger projects and teams who are looking to reduce conflicts, increase test coverage, and separate their code responsibility more cleanly.