Writing an app with VIPER architecture - Part 1

Last week I came across one of the many awesome frameworks on iOS. Having recently worked with MVVM and MVC, I was bit hesitant to learn VIPER, since more frameworks you learn, there is good chance you will forget the previous ones. However, I tried to keep my previous teaching as intact as possible while learning this new architecture.

Below are basic building blocks for VIPER. Every component has its own role in an architecture. The main goal of VIPER is single responsibility principle where it tries to keep responsibility for each module as minimal as possible and confined. This has two advantages

  • Change is easier
  • Unit testing is better and simple that heavy and mixed-up MVC approach

Let's look at the individual components now,

  1. View - Refers to ViewController or any subclass which houses UIView and/or UIViewController subclasses. This is strictly dumb part associated with VIPER. It does not know about complex logic or data fetching happening under the hood. Its job is to merely use the data sent by Presenter (We will see shortly below) and display it

  2. Presenter - Interacts directly with View and Interactor. Responsible for taking inputs from View and transfer it to Interactor. Interactor part then performs complex logic of fetching a data based on user input. Presenter can also perform type conversion if interactor requires data in specific format.

Presenter also receives a data, converts that into appropriate format and feeds to view. View is then responsible for displaying it to user. One of the examples of such case is when Interactor provides data models and presenter converts them to view models which are then fed to view

Presenter also performs an additional task of routing since it has direct access to view.

  1. Interactor - Interacts with Presenter bidirectionally to receive inputs, fetch data, perform complex calculations and push it back to view through presenter. Interactor is the main brain of architecture where logic happens. In ideal case interactor and presenter are the one that should be tested since view is merely a placeholder in this architecture.

Interactor also communicates directly with DataManager - A component responsible for fetching data from the network.

  1. Entity - It is also called a model. It is a representation of data downloaded or received from cache. Entity is an output after converting JSON to local struct or class. Fo example, entity could be an User object after login or list of products after user search action

  2. Router - Responsible for routing an app to appropriate screen. This is mainly based on the user action such as button tap or selection of specific option or simply going back in Navigation stack hierarchy. Since router has to have direct reference to ViewController associated with architecture, presenter acts as an intermediary between view and router

  3. DataManager - Communicates directly to interactor. Interactor, upon receiving input from view through presenter then contacts DataManager to fetch necessary data. When downloaded, DataManager passes the data back to interactor either through delegate callback or reactive binding

Enough with the theory, let's now play with a real example. An example I am going to demo is very simple login screen. It has following moving parts

  1. User enters username and password
  2. User presses Login button
  3. App sends pseudo request to server which takes second or two to get response back
  4. Upon the receipt of response, app updates an UI and make transition to next screen

First off, we will create a LoginController with an UI


class LoginViewController: UIViewController {

    let presenter: LoginPresentorProtocol
    var loginViewModel: LoginViewModel?
    let activityIndicatorView: UIActivityIndicatorView
    let loginSuccessfulLabel: UILabel
    let button: UIButton
    let usernameTextField: UITextField
    let passwordTextField: UITextField

    init(presenter: LoginPresentorProtocol) {
        self.presenter = presenter
        activityIndicatorView = UIActivityIndicatorView()
        activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
        activityIndicatorView.hidesWhenStopped = true
        loginSuccessfulLabel = UILabel()
        loginSuccessfulLabel.translatesAutoresizingMaskIntoConstraints = false
        loginSuccessfulLabel.numberOfLines = 0

        usernameTextField = UITextField()
        passwordTextField = UITextField()
        usernameTextField.translatesAutoresizingMaskIntoConstraints = false
        passwordTextField.translatesAutoresizingMaskIntoConstraints = false

        button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("Login", for: .normal)
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .red
        self.activityIndicatorView.activityIndicatorViewStyle = .whiteLarge
        self.view.addSubview(activityIndicatorView)
        self.view.addSubview(loginSuccessfulLabel)
        self.view.addSubview(button)
        self.view.addSubview(usernameTextField)
        self.view.addSubview(passwordTextField)

        usernameTextField.text = "foo"
        passwordTextField.text = "password"

        self.button.addTarget(self, action: #selector(performLogin), for: .touchUpInside)

        self.view.addConstraint(NSLayoutConstraint(item: activityIndicatorView, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1.0, constant: 0))
        self.view.addConstraint(NSLayoutConstraint(item: activityIndicatorView, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1.0, constant: 0))

        let views = ["label": loginSuccessfulLabel, "button": button, "username": usernameTextField, "password": passwordTextField]

        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]-|", options: [], metrics: nil, views: views))
        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[button]-|", options: [], metrics: nil, views: views))
        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[username]-|", options: [], metrics: nil, views: views))
        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[password]-|", options: [], metrics: nil, views: views))
        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-100-[username(>=0)]-[password(>=0)]-[label(>=0)]-[button(44)]", options: [], metrics: nil, views: views))
    }

    @objc private func performLogin() {
        self.presenter.executeFindItems(name: usernameTextField.text ?? "", password: passwordTextField.text ?? "")
    }

    private func goToNextScreen() {
        self.presenter.showList()
    }
}

Next comes the protocol named LoginViewProtocol which this LoginViewController class conforms to. These methods will be used as a callback from presenter to view for an update of any kind of data.

On the next line we will create a protocol and make our viewController to conform to it


protocol LoginViewProtocol: class {
    func showLoadingSpinner()
    func hideLoadingSpinner()
    func showUserWithSuccess(viewModel: LoginViewModel)
    func showUserWithError(_ error: String)
}

// Protocol conformence
extension LoginViewController: LoginViewProtocol {
    func showLoadingSpinner() {
        self.activityIndicatorView.startAnimating()
    }

    func hideLoadingSpinner() {
        self.activityIndicatorView.stopAnimating()
    }

    func showUserWithSuccess(viewModel: LoginViewModel) {
        self.loginViewModel = viewModel
        self.loginSuccessfulLabel.text = viewModel.successMessage
        self.title = viewModel.titleMessage
        let when = DispatchTime.now() + 1
        DispatchQueue.main.asyncAfter(deadline: when) {
            self.goToNextScreen()
        }
    }

    func showUserWithError(_ error: String) {
        self.loginSuccessfulLabel.text = error
        self.title = ""
        self.loginViewModel = nil
    }
}

Please note that viewController only has an access to viewModel which is a superset of model thus passed back from the presenter to view. Presenter and interactor never have access to UI elements and view never has access to dataManager or model object. This helps to separate out elements of concern

Since we have used two structures, User and LoginViewModel in the above example, below is their actual structures,


// Simple model
struct User {
    let username: String
    let password: String
}

// ViewModel
struct LoginViewModel {
    let user: User
    let successMessage: String
    let titleMessage: String
}

Please note that the difference between two. We get model from the interactor. Presenter's job is to convert this model into viewModel. Thus viewModel derives some of its values from model it receives from interactor. ViewModel is an object which is eventually communicated to view

Let's move on to Presenter now. Similar to view, presenter will also use the protocol named LoginPresentorProtocol to which it conforms to


// Protocol definition
protocol LoginPresentorProtocol: class {
    var view: LoginViewProtocol? { get set }
    var user: User? { get set }
    func executeFindItems(name: String, password: String)
    func showList()
    func itemDownloaded(user: User?)
}

// Protocol conformance
class LoginPresenter: LoginPresentorProtocol {

    weak var view: LoginViewProtocol?
    var user: User?

    let interactor: LoginInteractorProtocol
    let wireframe: LoginWireframeProtocol

    init(interactor: LoginInteractorProtocol, wireframe: LoginWireframeProtocol) {
        self.interactor = interactor
        self.wireframe = wireframe
    }
}
extension LoginPresenter {

    func executeFindItems(name: String, password: String) {
        self.view?.showLoadingSpinner()
        self.interactor.findUpcomingItems(name: name, password: password)
    }

    func showList() {
        guard let user = self.user else {
            self.view?.showUserWithError("User selected is nil, cannot proceed")
            return
        }
        self.wireframe.presentPosts(view: view!, user: user)
    }

    func itemDownloaded(user: User?) {
        self.user = user
        self.view?.hideLoadingSpinner()
        if let user = user {
            self.view?.showUserWithSuccess(viewModel: LoginViewModel(user: user, successMessage: "Login Successful", titleMessage: "Welcome Back"))
        } else {
            self.view?.showUserWithError("Failed to get user object")
        }
    }
}

Delegate methods used in the presenter will be used as a communication channel between interactor and view. As it is clear from above example, presenter is also responsible for calling delegate methods in router/wireframe which performs the task of routing from one screen to another

Since discussion about wireframe (I am calling it a wireframe based on the online tutorials I followed, but really it is just a router), We will make a protocol that Wireframe class will conform to and will be responsible for app routing


protocol LoginWireframeProtocol {
    func presentPosts(view: LoginViewProtocol, user: User)
}

class LoginWireframe: LoginWireframeProtocol {
    func presentPosts(view: LoginViewProtocol, user: User) {
        let dataManager: DataDownloader = DataManager()
        let interactor: DetailsInteractorProtocol = DetailsInteractor(dataManager: dataManager)
        let wireframe: DetailWireframeProtocol = DetailWireframe()
        let detailsPresenter: DetailsPresenterProtocol = DetailsPresenter(interactor: interactor, wireframeProtocol: wireframe, user: user)
        let detailsVC: DetailViewProtocol = DetailViewController(detailsPresenter: detailsPresenter)

        interactor.presenter = detailsPresenter
        detailsPresenter.view = detailsVC
        dataManager.itemsDetailsInteractor = interactor

        if let view = view as? UIViewController, let detailsVC = detailsVC as? UIViewController {
            view.navigationController?.pushViewController(detailsVC, animated: true)
        }
    }
}

In the example above, detailsVC is the destination viewController to which we want to make transition to. As it is clear from type information, it also conforms to DetailViewProtocol

Let's move on to interactor now. This is a piece which communicates with our low level component DataManager and pushes the data back to view. As with previous components, it will also involve a protocol which our interactor class will conform to


protocol LoginInteractorProtocol: class {
    var presenter: LoginPresentorProtocol? { get set }
    func findUpcomingItems(name: String, password: String)
    func itemDownloaded(user: User?)
}

class LoginInteractor: LoginInteractorProtocol {

    let dataManager: DataDownloader
    weak var presenter: LoginPresentorProtocol?

    init(dataManager: DataDownloader) {
        self.dataManager = dataManager
    }

    func findUpcomingItems(name: String, password: String) {
        self.dataManager.downloadItem(name: name, password: password)
    }

    func itemDownloaded(user: User?) {
        self.presenter?.itemDownloaded(user: user)
    }
}

Last, but not least - Let's outline our last and low level component DataManager. It will have direct contact only with Interactor part of VIPER and will be responsible for fetching a data and sending it over.


protocol DataDownloader: class {
    func downloadItem(name: String, password: String)
    var listItemsInteractor: LoginInteractorProtocol? { get set }
}

class DataManager: DataDownloader {

    weak var listItemsInteractor: LoginInteractorProtocol?

    func downloadItem(name: String, password: String) {
        let when = DispatchTime.now() + 0.5
        DispatchQueue.main.asyncAfter(deadline: when) {
            if name == "foo" && password == "password" {
                self.listItemsInteractor?.itemDownloaded(user: User(username: name, password: "password"))
            } else {
                self.listItemsInteractor?.itemDownloaded(user: nil)
            }
        }
    }
}

Now, since we have all the code let's put it together. All in all, the components will have references to each other. We will also declare some of variable with weak keyword to avoid retain cycles. Components won't have direct reference to each other, but they will talk to each other in the form of protocol and are completely agnostic of underlying datatype


// Initialize the DataManager object which conforms to DataDownloader protocol
let dataManager: DataDownloader = DataManager()

// Initialize an interactor named LoginInteractor which conforms to LoginInteractorProtocol
let interactor: LoginInteractorProtocol = 
LoginInteractor(dataManager: dataManager)

// Initiate a wireframe object of type LoginWireframe which conforms to LoginWireframeProtocol and has access to view part of VIPER architecture.
let wireFrame: LoginWireframeProtocol = LoginWireframe()

// Initialize the presenter object of type LoginPresenter which conforms to LoginPresenterProtocol and access to interactor and wireframe objects.
let presenter: LoginPresentorProtocol = LoginPresenter(interactor: interactor, wireframe: wireFrame)

// Initialize a viewController of type LoginViewController which conforms to LoginViewProtocol and has reference to presenter object.
let vc: LoginViewProtocol & LoginViewController = LoginViewController(presenter: presenter)

// Presenter should have reference back to view
presenter.view = vc

// Interactor will maintain reference to presenter
interactor.presenter = presenter

// DataManager object will have reference to interactor
dataManager.listItemsInteractor = interactor

// Once our viewController is initialized with necessary components, we are ready to use it in our application
self.window?.rootViewController = UINavigationController(rootViewController: vc)

After we put it all together, below are app states on how it looks like before and after login is completed with error and success states.

Initial

Login Success State

Login Error State

I have hosted a code on GitHub if you want to play with it

You can find the second part of VIPER architecture series here

References:

objc.io - VIPER architecture

Mindorks Blog