VIPER architecture: Our best practices to build an app like a boss

The dev team at Cheesecake Labs has been using VIPER for iOS and Android mobile app development for over one year and we just love this clean architecture!

This article summarizes our best practices on the VIPER architecture, using code examples from our VIPER boilerplate. The code samples used here are in Swift, but all concepts mentioned may be applied to an Android project developed with VIPER, either using Java or Kotlin.

If you still need to get familiar with basic VIPER concepts and how this architecture can make your code more organized and scalable, I suggest you read the following articles:

So, are you ready to learn our best practices on how to build an app with VIPER?
Funny image of a man vibrating and saying let us go for VIPER

Project structure for real VIPER modules decoupling

Image of Xcode Navigation Bar with the folder structure for iOS VIPER project

VIPER’s folder structure for iOS

You can see that we keep all VIPER’s modules as decoupled as possible, saving us from future headaches when the project grows or specification changes. If you delete any of them, you should get several errors just on the Routers that reference that model – but not on Views, Presenters, Interactors, Data Managers or Entity classes.

One key point to help you truly decouple your modules is to keep all entities on a separate folder, linking them to the project itself and not to any specific module.

Also, using Data Managers to perform API requests and manipulate local database is an excellent way of increasing the project organization, but requires some attention:

  1. Keep Data Managers inside the Utils folder, separating them from the modules;
  2. Group methods for similar entities – such as User, Profile and CompanyProfile – into one Data Manager to avoid the overhead of creating one Data Manager for each entity;
  3. Split each Data Manager into Local and API classes to make the code on the Interactors much more readable:

class MainSearchInteractor {
    
    // Properties
    weak var output: MainSearchInteractorOutput?
    var apiDataManager = ProfileApiDataManager()
    var localDataManager = ProfileLocalDataManager()
}

extension MainSearchInteractor: MainSearchUseCase {
  
    // Code below show how interactor get data from API and then saves it on local DB with separate data managers
    func searchProducts(with searchTerm: String, onPage page: Int) {
        
        self.apiDataManager.searchProducts(with: searchTerm, forPage: page) { (products) in
            if let products = products {
                self.localDataManager.updateSearchResultFavorites(products) { (products) in
                    self.output?.onFetchProductsSuccess(Array(products), shouldAppend: page != 1)
                }
            } else {
                self.output?.onFetchProductsSuccess(nil, shouldAppend: page != 1)
            }
        }
    }
}

Generating VIPER’s files faster and furiously

If you’ve already developed using VIPER, you’ve had the bad experience of creating all the 20+ Swift files needed for a simple screen with three tabs on the navigation bar. But there’s a light at the end of the tunnel: this amazing Xcode plugin which automates the generation of all VIPER files for one module with three clicks.

If you think that that’s too much, meet Generamba: a code generator designed to create VIPER modules from the terminal, quite easy to customize for any other classes.

Define contracts with protocols

Just like for us humans, contracts on VIPER are a voluntary agreement between two parties (module components) concerning the rights (methods) and duties (arguments) that arise from agreements. At Cheesecake Labs, we use protocols to define the methods that a module component can call from other components on the same module.

However, before start writing the code for a new View or Presenter, for example, think about the information flow between both components and declare their methods on the Contract first.


// MainSearchContract.swift
import Foundation

protocol MainSearchView: BaseView {
    func showCustomError(_ message: String?)
    func updateVisibility(onSearchController willBeActive: Bool)
    func showSearchResult(_ products: [Product]?, shouldAppend: Bool)
}

protocol MainSearchPresentation: class {
    func onViewDidLoad()
    func onWillPresentSearchController()
    func onSearchTermChanged(to searchTerm: String)
    func onProductFavoriteChanged(_ product: Product, to isFavorite: Bool)
    func onProductSelected(_ product: Product)
    func onInfiniteScrollTriggered()
}

protocol MainSearchUseCase: class {
    func searchProducts(with searchTerm: String, onPage page: Int)
    func updateProductFavorited(_ product: Product, to newValue: Bool)
}

protocol MainSearchInteractorOutput: class {
    func onFetchProductsSuccess(_ products: [Product]?, shouldAppend: Bool)
    func onFetchProductsFailure(message: String)
}

protocol MainSearchWireframe: class {
    func showProductScreen(delegate: ProductScreenDelegate, product: Product?)
}

The Xcode plugin mentioned before will also create a ModuleNameContract.swift file with all protocols, waiting for your declaration of the necessary methods. Once those protocols are defined, you have complete control of the information flow between the components of a VIPER module.

Image of a judge showing a contract paper and asking whose signature is that, making reference to a VIPER contract

Automate modules initialization on the Router

Before presenting the View of a VIPER module, you need to make sure all components have been properly initialized. I can think of at least 3 very different ways of doing it, but the flow below is the best option we’ve came up with. The ace in the hole here is to have a static function on each Router to initialize its own module together with some UIViewController and UIStoryboard extensions. Then, if module A wants to present Module B:

  1. Module A’s Router will call Module B’s static function to initialize all of its components, returning a View.
  2. Module A’s Router presents Module B’s View.

As simple as that. Having the module initialization code on its own Router will eliminate a bunch of code repetition, specially for huge projects.

You need to create these extensions once:


// ReusableView.swift
protocol ReusableView: class {}

extension ReusableView {
    static var reuseIdentifier: String {
        return String(describing: self)
    }
}

// UIViewController.swift
extension UIViewController: ReusableView { }

// UIStoryboard.swift
extension UIStoryboard {
    func instantiateViewController() -> T where T: ReusableView {
        return instantiateViewController(withIdentifier: T.reuseIdentifier) as! T
    }
}

And then, leave initialization code on the router of each VIPER module:


// MainSearchRouter.swift
class MainSearchRouter {

    // MARK: Properties
    weak var view: UIViewController?

    // MARK: Static methods
    static func setupModule() -> MainSearchViewController {
        let viewController = UIStoryboard(name: MainSearchViewController.storyboardName, bundle: nil).instantiateViewController() as MainSearchViewController
        let presenter = MainSearchPresenter()
        let router = MainSearchRouter()
        let interactor = MainSearchInteractor()

        viewController.presenter =  presenter

        presenter.view = viewController
        presenter.router = router
        presenter.interactor = interactor

        router.view = viewController

        interactor.output = presenter

        return viewController
    }
}

It might seem like a lot of steps, but good news: the aforementioned plugin automates that for us as well! 🙂

However, you’ll need to take some additional steps if you want to fit a UITabBarController or a UIPageViewController into the VIPER architecture. If you need any help, just drop a comment on this post and I’ll prepare a specific Gist for you.

If you’ve came up this far, dear reader, you’re really avid for knowledge. So I’ll give you 3 advices to make sure you’ve fully understood the responsibilities of the Router:

  1. If you need to open a URL when the user clicks a button, call UIApplication.shared.openURL(url) on the Router because you’re navigating (i.e. routing) out of your current module;
  2. The same concept applies for social media sharing: call UIActivityViewController from the Router because iOS will send the user to a View or app out of your current module;
  3. If you’re only calling an Action Sheet to get some user input, that’s an UI component added to your current module. So you can call it from your View, and enjoy straightforward callbacks from the UIAlertController.

Use delegates to send data between VIPER modules

You’ve probably faced a situation where a field on Module A is filled with the selected item of the Module B. So Module A calls Module B when the user clicks the field, and Module B returns the selected item to the existing Module A through the delegate.

Delegates are an awesome approach to send information back and forth between VIPER modules:


// 1. Declare which messages can be sent to the delegate

// ProductScreenDelegate.swift
protocol ProductScreenDelegate {
    //Add arguments if you need to send some information
    func onProductScreenDismissed()
    func onProductSelected(_ product: Product?)
}

// 2. Call the delegate when you need to send him a message

// ProductPresenter.swift
class ProductPresenter {

    // MARK: Properties
    weak var view: ProductView?
    var router: ProductWireframe?
    var interactor: ProductUseCase?
    var delegate: ProductScreenDelegate?
}

extension ProductPresenter: ProductPresentation {

    //View tells Presenter that view disappeared
    func onViewDidDisappear() {

        //Presenter tells its delegate that the screen was dismissed
        delegate?.onProductScreenDismissed()
    }
}

// 3. Implement the delegate protocol to do something when you receive the message

// ScannerPresenter.swift
class ScannerPresenter: ProductScreenDelegate {

    //Presenter receives the message from the sender
    func onProductScreenDismissed() {

        //Presenter tells view what to do once product screen was dismissed
        view?.startScanning()
    }
    ...
}

// 4. Link the delegate from the Product presenter in order to proper initialize it

// File ScannerRouter.swift
class ProductRouter {

    static func setupModule(delegate: ProductScreenDelegate?) -> ProductViewController {
        ...
        let presenter = ScannerPresenter()

        presenter.view = view
        presenter.interactor = interactor
        presenter.router = router
        presenter.delegate = delegate // Add this line to link the delegate
        ...
        }
}

 

And avoid dictionaries to send information between VIPER components

Using a POSO (Plain Old Swift Object) to send information between VIPER’s components is the best approach if you want to be 100% compliant with the VIPER architecture. But sending the Entity itself between VIPER components works fine and removes the overhead of creating POSOs.

Anyway, avoid sending this data using dictionaries if you don’t want to get lost with key names when your project starts growing and changing.

Focus on the VIPER mindset

If you want to take the most of this architecture, it’s important to keep your team completely in sync with the responsibilities of each component of a VIPER module.

Even after understanding the role of each specific component, our team still faced some doubts, mostly influenced by previous experience with MVC.

Homer Simpson is telling other people that he wants to make a few things clear, making reference to VIPER responsabilities.

  1. The View is the one that handles UI elements: it imports UIKit and implements all logic regarding UI elements from its module. TableView logics, for example, are implemented on the View. If you want to make your code more readable, split TableView logics on extensions. If you want to make your project even more concise, use a TableViewDataManager;
  1. The presenter does not import UIKit and does not handle UI elements, but it does prepare the data in the format required by the view and take decisions based on UI events from the view. Do not manipulate any UI element on the presenter, it shouldn’t handle them;
  1. The Interactor can also prepare the data, but for the database. Once the ApiDataManager fetches some data, for example, the Interactor can do some sorting or filtering before asking the LocalDataManager to save the data. But note that the Interactor doesn’t know the view, so it has no idea how the data should be prepared for the view.

Wrapping up

The product team decided to drop out a feature from your project? Or your small project started growing huge? Use proper VIPER architecture and avoid future headaches!

Automating VIPER files creation and modules initialization will eliminate the overhead of working with this – complex at first sight – but clear and awesome architecture. Android developers can also use it as well.

We’ve seen that our approach to VIPER architecture is actually composed of VIPRC modules (View-Interactor-Presenter-Router-Contract), while Entities are decoupled from the modules, along with Data Managers. I know the name VIPRC is not sexy at all, but it allows you to build an app like a boss.

Do you have any other tips for using VIPER architecture on iOS and Android app development? Feel free to share your experience!

About the author.

Marcelo Gracietti
Marcelo Gracietti

Jumped drillships to join great friends on their amazing mission, exploring his developer/entrepreneur skills. Loves traveling and can cook a lasagna better than his grandmother.