VIPER Architecture for iOS Apps

Surbhi Gupta
YML Innovation Lab
Published in
4 min readMay 18, 2023

What is a Good Architecture?

Architecture serves as a blueprint for a system. It provides a mechanism to communicate between various components and abstracts the complexity. It is important to separate out code components so that they are identifiable, serve a single purpose/responsibility and also fit logically. Code should be scalable, stable upon expansion, and flexible, which brings in the need for a good architecture.

A good architecture not only helps in the success of the product but also allows for easy maintenance and makes people’s lives easier.

This article will talk about VIPER architecture, which is used to build many large projects.

What is VIPER?

VIPER is an architectural pattern like MVC and MVVM. One of the major issues with MVC and MVVM is bulky controllers. VIPER solves this problem by following Single Responsibility Principle and making the code more modular.

Flow of Control

Each module created with VIPER has five classes, each with their own responsibilities. No class does anything other than the specified role. These classes are listed below:

View: It is responsible for only maintaining and displaying the view. It picks up any user interactions and passes the information to the presenter to decide the next step. Unlike other architecture patterns like MVC or MVVM, its sole responsibility is to manage and show the UI.

Presenter: It is the only class that interacts with all other components except Entity. It is the decision-making engine for routing the flow.

Interactor: It interacts with Entity and contains business logic. This class is where all the API calls and database interactions are added.

Entity: Data model that is used by the interactor.

Router: It does all the wire-framing or navigation from one module to another based on the information passed by the presenter.

An ideal folder structure would be like below:

Ideal Folder Structure
Ideal Folder Structure with VIPER

The communication between various components follows a delegation pattern. One component calls the other through a protocol.

There are typically five protocols declared:

protocol LoginViewToPresenterProtocol: class {
// View communicates to Presenter.
// Handle User interactions
}

protocol LoginPresenterToViewProtocol: class {
// Presenter communicates with View
// Communciates UI changes and actions in return to UI interactions
}

protocol LoginPresenterToInteractorProtocol: class {
// Presenter communicates with Interactor
// Asks to compute any business logic and network interations and data fetches
}

protocol LoginInteractorToPresenterProtocol: class {
// Interactor communicates with Presenter
// Responds with output of business logic and data
}

protocol LoginPresenterToRouterProtocol: class {
// Presenter communciates to Router
// Communicates which next module to load and how the flow will navigate.
}

Now, let’s look at how the data would flow between components in a sample login module.

The user taps on the login button. The LoginViewController would receive the button action and will pass the data to the Presenter to take necessary actions:

 @IBAction func onLoginTapped(_ sender: Any) {
presenter?.performLogin(with: ["username": userEnteredusername, "password": userEnteredpassword])
}

The Presenter being the brain of the system would decide what is the next step. It will then pass the data to the Interactor asking it to make the API call:

func performLogin(with userCredentials: [String: String]) {
interactor?.authenticate(userCredential: userCredentials)
}

The Interactor now makes the API call to authenticate the credentials and based on the response will call the Presenter with either a success or a failure:

func authenticate(userCredential: [String: Any]?) async {
do { // Make API call
let authSuccess = try await NetworkManager.shared.autheticateuser(userCredential)
presenter?.authenticationSuccess()
} catch let error {
presenter?.authenticationFailed(withError: error)
}
}

If the authentication is successful, the Presenter would call the Router to navigate to the Dashboard view controller:

func authenticationSuccess() {
router?.showDashboardModule()
}

The Router when called will perform the navigation to the DashboardView:

func showDashboardModule(from view: LoginPresenterToViewProtocol) {
guard let loginVC = view as? LoginViewController else {
return
}
if let dashboardVC = DashboardVC.load() { // Method which wil instantiateViewController
loginVC.navigationController?.pushViewController(dashboardVC, animated: true)
}
}

If the authentication is fails, the Presenter would pass the error message to the view to display:

func authenticationFailed(withError error: NSError) {
view?.loginFailed(withError: error)
}

The View would now show an alert message to the user:

func loginFailed(withError error: NSError) {
// Compute the error message and error title
self.showAlert(with: errorTitle, message: errorMessage)
}

In the above way, the complete cycle gets executed through the delegates and protocols, and each component does only specific required actions.

When to Choose VIPER?

While VIPER can offer significant benefits, it’s important to consider its potential drawbacks. One potential challenge is that VIPER can be bulky and generate a large number of files in the codebase. If not properly organized, these files can become difficult to manage.

However, VIPER is particularly well-suited for complex and large projects, where its structured approach can greatly enhance maintainability. On the other hand, for small applications, VIPER may be considered overkill, as its extensive architecture might not be necessary. In such cases, its better to chose MVVM.

Projects with multiple developers can greatly benefit from VIPER since it helps mitigate conflicts in the codebase by enforcing a clear separation of concerns. Nonetheless, it’s worth noting that VIPER may initially require additional time and effort from developers as they need to carefully plan and define the code’s division before diving into the development phase.

From personal experience, I have utilized VIPER in a complex program, and over time, it has proven to simplify my work and improve the overall project progression.

Happy Coding and chose your architecture wisely :)

--

--

Surbhi Gupta
YML Innovation Lab

Engineering Manager, Solution Architect, blogger and a firm believer