This is the first post in a series of posts about iOS project architecture. In these posts I do not intend to go into details about the architectural patterns’ pros and cons, instead, the focus will be on outlining different architectural structures, which may be applied in an iOS project. I do however intend to write a more opinionated piece as a summary/conclusion for the different concepts introduced throughout this series later on.
In this post, I will introduce the VIPER architectural pattern. VIPER may be used as a design pattern on a variety of platforms, in this post, I will be focusing on using VIPER in an iOS project.
VIPER stands for the different components of the architectural pattern; View, Interactor, Presenter, Entity and Routing. The core principle of VIPER is to separate the app’s functionality into these different components, in order to achieve low coupling between the app’s different components and to follow the single responsibility principle.
Apart from the components included in VIPER that are mentioned above, the VIPER pattern also takes use of helpers, which manages the logic between external sources, such as a database, and the iOS application, these components can be referred to as Network Managers.
VIPER’s different components
To start off, I will go through the different components in VIPER and their responsibilities. While describing VIPER’s different components, I will provide examples of where to find the different components in a traditionally set up MVC iOS project (where applicable).
Once the components have been introduced, I will focus on how the different components come together in a project and how the components relate to one another.
The view is responsible for displaying information to the user of the app. Since the view is what the user sees, this is an important component which provides the user with a direct interface to the application - in a traditional iOS project, this may be a view in a storyboard or in a .xib file. The view is therefore also responsible for providing the app with a way for a user to input information.
The interactor contains the app’s business logic of how to handle data. The interactor is not directly responsible for retrieving data from a server, this is often allocated to one or several Network Managers. However, the interactor is responsible for interacting with these Network Managers. Since a specific app may require data to be retrieved from multiple, different data sources, the interactor may initialize network requests through multiple, different network managers. It’s up to the interactor to retrieve the required data for the app’s specific use case, from the different data points, according to the app’s business logic. Once data has been retrieved from all external servers via the network managers, the interactor handles any data manipulation that may be necessary.
Note: If you are familiar with a modern MVVM architecture, the responsibility of an interactor may be compared to the responsibility of MVVM’s ViewModel.
The presenter’s responsibility is to apply the logic of how content should be displayed. The presenter uses the data provided by the interactor and then applies the necessary logic of how this data should be presented in a view. The presenter is also responsible for the logic of how the application should respond to user inputs and user interactions. This responsibility is often given to a ViewController in the context of an iOS project.
An entity is a data storage model. This can either be a persistent storage model - such as a Core Data object - or it can be a more temporary storage model - such as a Struct, Class or an Enum. The entity is not supposed to contain any logic related to formatting of data before displaying it, neither should it contain logic about how to retrieve the data (e.g. network request). An entity simply contains variables that are specific to the model it represents. The responsibility of how to display the data of an entity is instead given to the presenter, and a network manager is responsible for how to retrieve data.
At last, the routing component is responsible for the logic required for, and the navigation between the app’s different views. This component describes what is required to display each of the app’s views. Once all the dependencies required for a specific view is provided/created, routing initializes the view, providing these dependencies (this is called dependency injection) and then presents the view.
Note: The routing component takes the responsibility of navigating between different views, this is a responsibility that is traditionally split between a storyboard segue and a ViewController (in a prepareForSegue function). An app that’s using the VIPER architecture therefore usually doesn’t contain any segues between a storyboard’s different views.
According to the VIPER architecture, no classes other than a data manager should have any awareness of Core Data, or of Core Data objects. Instead, a data manager class converts the Core Data object to a PONSO (Plain Old NSObject) before passing it to any other class, like an interactor class.
Using Modules with VIPER
A screen or a set of screens may be added to a Module. A module can simply be a folder in the project that contains the functionality required for a specific screen. The module pattern may be useful for replacing or reusing specific behavior for different platforms. For instance, if a specific functionality only exists for an iPad app, it is easily replaceable by a different module for an iPhone application.
What will this structure look like in an iOS project?
So far through this article, I have identified the different components of VIPER, as well as these components’ different responsibilities. Let’s go through how these components interact with one another and how they fit together in a project in more detail.
The fact that each of the components in the VIPER model are dedicated to a specific task, makes it easy for us to combine the components to fulfill a task required for the app to function, as well as re-using the components in different parts of the app. As the diagram above demonstrates, the different components interact with one another, to reach a specific goal.
While the View provides a user with a specific screen of the app, the Presenter is playing an important role in handling decision making of how data should be displayed in a view and what to do with data that a user enters to a view. The presenter gets data that should be displayed in a specific view from the Interactor and formats the data before it’s being displayed in the view. When the user of our app interacts with the view, it’s the presenter’s responsibility to decide how the view should change, to provide the user with necessary feedback.
If the user input should require being stored for later retrieval, the presenter forwards the data to an interactor. The interactor decides how the data should be formatted by applying relevant business logic, before either handing it over to a network manager (for external storage of the information) or to an Entity.
An app often contains multiple screens, with different functionalities, dedicated to fulfilling a specific use case. Each of these screens (or a set of related screens) are each made up of a View, an Interactor, a Presenter, an Entity and a Routing component, which combined can be referred to as a Module. Since some screens may require the same entities or the same interactor as a different screen, a VIPER component may be reused across different modules.
Routing can exist on different levels within the application. There can be a router specific for a module, providing the navigation between a collection of screens. A router may also exist on higher, more overarching app level, providing the navigational functionality between the apps higher level, main views (for instance, the creation of views and the routing to these views, from a tab bar).