Radu Dan
VIPER Banner

Architecture Series - View Interactor Presenter Entity Router (VIPER)

Motivation

Before starting to develop an iOS app, we have to think about the structure of the project. We need to consider how we add those pieces of code together so they make sense later on - when we come back and revisit a part of the app - and how to form a known ā€œlanguageā€ with the other developers.

In this implementation article, we will transform our Football Gather app into using a VIPER codebase architecture.

If you missed the other articles you can access them below or you can find the links at the end of this article.

Are you impatient and just want to see the code? No worries! You have it available on GitHub.

Following the same approach as we did for the other posts, we will first say a few things about this pattern and why it's useful. Then we will see the actual implementation.
Finally, we will show some figures about compilation and build times, check how easy was to write unit tests and state our conclusions.

Why an architecture pattern for your iOS app

The most important thing to consider is to have an app that can be maintainable. You know the View goes there, this View Controller should do X and not Y. And more important the others know that too.

Here are some advantages of choosing a good architecture pattern:

  • Easier to maintain
  • Easier to test the business logic
  • Develop a common language with the other teammates
  • Separate the responsibility of your entities
  • Less bugs

Defining the requirements

Given an iOS application with 6-7 screens, we are going to develop it using the most popular architecture patterns from the iOS world: MVC, MVVM, MVP, VIPER, VIP and Coordinators.

The demo app is called Football Gather and is a simple way of friends to track score of their amateur football matches.

Main features

  • Ability to add players in the app
  • You can assign teams to the players
  • Edit players
  • Set countdown timer for matches

Screen Mockups

Football Gather Mockups

Backend

The app is powered by a web app developed in Vapor web framework. You can check the app here (Vapor 3 initial article) and here (Migrating to Vapor 4).

Dude, whereā€™s my VIPER?

VIPER stands for View-Interactor-Presenter-Entity-Router.

We saw in MVP what the Presenter layer is and what it does. This concept applies as well for VIPER, but has been enhanced with a new responsibility, to get data from the Interactor and based on the rules, it will update / configure the View.

View

Must be as dumb as possible. It forwards all events to the Presenter and mostly should do what the Presenter tells it to do, being passive.

Interactor

A new layer has been introduced, and in here we should put everything that has to do with the business rules and logic.

Presenter

Has the responsibility to get data from the Interactor, based on the userā€™s actions, and then handle the View updates.

Entity

Is the Model layer and is used to encapsulate data.

Router

Holds all navigation logic for our application. It looks more like a Coordinator, without the business logic.

Communication

When something happens in the view layer, for example when the user initiates an action, it is communicated to the Presenter.

The Presenter asks the Interactor for the data needed by the user. The Interactor provides the data.

The Presenter applies the needed UI transformation to display that data.

When the model / data has been changed, the Interactor will inform the Presenter.

The Presenter will configure or refresh the View based on the data it received.

When users navigate through different screens within the app or take a different route that will change the flow, the View will communicate it to the Presenter.

The Presenter will notify the Router to load the new screen or load the new flow (e.g. pushing a new view controller).

Extended VIPER

There are a few concepts that are commonly used with VIPER architecture pattern.

Modules

Is a good idea to separate the VIPER layers creation from the Router and introduce a new handler for module assembly. This is done most likely with a Factory method pattern.


    protocol AppModule {
        func assemble() -> UIViewController?
    }

    protocol ModuleFactoryProtocol {
        func makeLogin(using navigationController: UINavigationController) -> LoginModule
        func makePlayerList(using navigationController: UINavigationController) -> PlayerListModule
    }

And the concrete implementation for our app:


    struct ModuleFactory: ModuleFactoryProtocol {
        func makeLogin(using navigationController: UINavigationController = UINavigationController()) -> LoginModule {
            let router = LoginRouter(navigationController: navigationController, moduleFactory: self)
            let view: LoginViewController = Storyboard.defaultStoryboard.instantiateViewController()
            return LoginModule(view: view, router: router)
        }

        func makePlayerList(using navigationController: UINavigationController = UINavigationController()) -> PlayerListModule {
            let router = PlayerListRouter(navigationController: navigationController, moduleFactory: self)
            let view: PlayerListViewController = Storyboard.defaultStoryboard.instantiateViewController()
            return PlayerListModule(view: view, router: router)
        }
    }

We will see later more source code.

TDD

This approach does a good job from a Clean Code perspective, and you develop the layers to have a good separation of concerns and follow the SOLID principles better.

So, TDD is easy to achieve using VIPER.

  • The modules are decoupled.
  • There is a clear separation of concerns.
  • The modules are are neat and clean from a coding perspective.

Code generation tool

As we add more modules, flows and functionality to our application, we will discover that we write a lot of code and most of it is repetitive.

There is a good idea to have a code generator tool for your VIPER modules.

Solving the back problem

We saw that when applying the Coordinator pattern we had a problem when navigating back in the stack, to a specific view controller.
In this case, we need to think of a way if in our app we need to go back or send data between different VIPER modules.

This problem can be easily solved with Delegation.

For example:


    protocol PlayerDetailsDelegate: AnyObject {
        func didUpdatePlayer(_ player: Player)
    }

    // we make the Presenter the delegate of PlayerDetailsPresenter so we can refresh the UI when a player has been changed.
    extension PlayerListPresenter: PlayerDetailsDelegate {
        func didUpdatePlayer(_ player: Player) {
            viewState = .list
            configureView()
            view?.reloadData()
        }
    }

More practical examples we are going to see in the section Applying to our code.

When to use VIPER

VIPER should be used when you have some knowledge about Swift and iOS programming or you have experienced or more senior developers within your team.

If you are part of a small project, that will not scale, then VIPER might be too much. MVC should work just fine.

Use it when you are more interested in modularising and unit test the app giving you a high code coverage.
Donā€™t use it when you are a beginner or you donā€™t have that much experience into iOS development.
Be prepared to write more code.

From my point of view, VIPER is great and I really like how clean the code looks. Is easy to test, my classes are decoupled and the code is indeed SOLID.

For our app, we separated the View layer into two components: ViewController and the actual View.
The ViewController acts as a Coordinator / Router and holds a reference to the view, usually set as an IBOutlet.

Advantages

  • The code is clean, SRP is at its core.
  • Unit tests are easy to write.
  • The code is decoupled.
  • Less bugs, especially if you are using TDD.
  • Very useful for complex projects, where it simplifies the business logic.
  • The modules can be reusable.
  • New features are easy to add.

Disadvantages

  • You may write a lot of boilerplate code.
  • Is not great for small apps.
  • You end up with a big codebase and a lot of classes.
  • Some of the components might be redundant based on your app use cases.
  • App startup will slightly increase.

Applying to our code

There will be major changes to the app by applying VIPER.

We decided to not keep two separate layers for View and ViewController, because one of these layer will become very light and it didnā€™t serve much purpose.

All coordinators will be removed.

First, we start by creating an AppLoader that will load the first module, Login.


    struct AppLoader {
        private let window: UIWindow
        private let navigationController: UINavigationController
        private let moduleFactory: ModuleFactoryProtocol

        init(window: UIWindow = UIWindow(frame: UIScreen.main.bounds),
             navigationController: UINavigationController = UINavigationController(),
             moduleFactory: ModuleFactoryProtocol = ModuleFactory()) {
            self.window = window
            self.navigationController = navigationController
            self.moduleFactory = moduleFactory
        }

        // This function is similar with the one we had for Coordinators, start().
        func build() {
            let module = moduleFactory.makeLogin(using: navigationController)
            let viewController = module.assemble()
            setRootViewController(viewController)
        }

        private func setRootViewController(_ viewController: UIViewController?) {
            window.rootViewController = navigationController

            if let viewController = viewController {
                navigationController.pushViewController(viewController, animated: true)
            }

            window.makeKeyAndVisible()
        }
    }

We allocate AppLoader in AppDelegate and call the function build() when the app did finish launching.


    class AppDelegate: UIResponder, UIApplicationDelegate {

        private lazy var loader = AppLoader()

        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            loader.build()
            return true
        }

    ..
    }

We saw earlier, how we use ModuleFactory to create VIPER modules. We provide an interface for all modules that require assemble in our app.


    protocol ModuleFactoryProtocol {
        func makeLogin(using navigationController: UINavigationController) -> LoginModule
        func makePlayerList(using navigationController: UINavigationController) -> PlayerListModule
        func makePlayerDetails(using navigationController: UINavigationController,
                               for player: PlayerResponseModel,
                               delegate: PlayerDetailDelegate) -> PlayerDetailModule
        func makePlayerEdit(using navigationController: UINavigationController,
                            for playerEditable: PlayerEditable,
                            delegate: PlayerEditDelegate) -> PlayerEditModule
        func makePlayerAdd(using navigationController: UINavigationController, delegate: PlayerAddDelegate) -> PlayerAddModule
        func makeConfirmPlayers(using navigationController: UINavigationController,
                                playersDictionary: [TeamSection: [PlayerResponseModel]],
                                delegate: ConfirmPlayersDelegate) -> ConfirmPlayersModule
        func makeGather(using navigationController: UINavigationController,
                        gather: GatherModel,
                        delegate: GatherDelegate) -> GatherModule
    }
    

We have a struct ModuleFactory that is the concrete implementation of the above protocol.


    struct ModuleFactory: ModuleFactoryProtocol {
        func makeLogin(using navigationController: UINavigationController = UINavigationController()) -> LoginModule {
            let router = LoginRouter(navigationController: navigationController, moduleFactory: self)
            let view: LoginViewController = Storyboard.defaultStoryboard.instantiateViewController()
            return LoginModule(view: view, router: router)
        }
    /// other functions
    ā€¦
    }

Letā€™s see how LoginModule is created.


    final class LoginModule {

        // Set the dependencies
        private var view: LoginViewProtocol
        private var router: LoginRouterProtocol
        private var interactor: LoginInteractorProtocol
        private var presenter: LoginPresenterProtocol

        // Optionally, provide default implementation for your protocols with concrete classes
        init(view: LoginViewProtocol = LoginViewController(),
             router: LoginRouterProtocol = LoginRouter(),
             interactor: LoginInteractorProtocol = LoginInteractor(),
             presenter: LoginPresenterProtocol = LoginPresenter()) {
            self.view = view
            self.router = router
            self.interactor = interactor
            self.presenter = presenter
        }

    }

    // Reference your layers
    extension LoginModule: AppModule {
        func assemble() -> UIViewController? {
            presenter.view = view
            presenter.interactor = interactor
            presenter.router = router

            interactor.presenter = presenter

            view.presenter = presenter

            return view as? UIViewController
        }
    }

Every module will have a function assemble() that is needed when implementing the AppModule protocol.

In here, we create the references between the VIPER layers:

  • We set the view to the presenter (weak link).
  • Presenter holds a strong reference to the Interactor.
  • Presenter holds a strong reference to the Router.
  • Interactor holds a weak reference to the Presenter.
  • Our View holds a strong reference to the Presenter.

We set the weak references to avoid, of course, retain cycles which can cause memory leaks.

Every VIPER module within our app is assembled in the same way.

LoginRouter has a simple job: present the players after the user logged in.


    final class LoginRouter {

        private let navigationController: UINavigationController
        private let moduleFactory: ModuleFactoryProtocol

        // We inject the module factory so we can create and assemble the next screen module (PlayerList).
        init(navigationController: UINavigationController = UINavigationController(),
             moduleFactory: ModuleFactoryProtocol = ModuleFactory()) {
            self.navigationController = navigationController
            self.moduleFactory = moduleFactory
        }

    }

    // When the user logged in, route to PlayerList
    extension LoginRouter: LoginRouterProtocol {
        func showPlayerList() {
            let module = moduleFactory.makePlayerList(using: navigationController)

            if let viewController = module.assemble() {
                navigationController.pushViewController(viewController, animated: true)
            }
        }
    }

One important aspect that we missed when applying MVP to our code, was that we didnā€™t made our View passive. The Presenter acted more like a ViewModel in some cases.

Letā€™s correct that and make the View as passive and dumb as we can.

Another thing that we did, was to split the LoginViewProtocol into multiple small protocols, addressing the specific need:


    typealias LoginViewProtocol = LoginViewable & Loadable & LoginViewConfigurable & ErrorHandler

    protocol LoginViewable: AnyObject {
        var presenter: LoginPresenterProtocol { get set }
    }

    protocol LoginViewConfigurable: AnyObject {
        var rememberMeIsOn: Bool { get }
        var usernameText: String? { get }
        var passwordText: String? { get }

        func setRememberMeSwitch(isOn: Bool)
        func setUsername(_ username: String?)
    }

We combined all of them by using protocol composition and named them with a typealias. We use the same approach for all of our VIPER protocols.

The LoginViewController is described below:


    final class LoginViewController: UIViewController, LoginViewable {

        // MARK: - Properties
        @IBOutlet weak var usernameTextField: UITextField!
        @IBOutlet weak var passwordTextField: UITextField!
        @IBOutlet weak var rememberMeSwitch: UISwitch!
        lazy var loadingView = LoadingView.initToView(view)

        // We can remove the default implementation of LoginPresenter() and force-unwrap the presenter in the protocol definition. We used this approach for some modules.
        var presenter: LoginPresenterProtocol = LoginPresenter()

        // MARK: - View life cycle
        override func viewDidLoad() {
            super.viewDidLoad()
            presenter.viewDidLoad()
        }

        // MARK: - IBActions
        @IBAction private func login(_ sender: Any) {
            presenter.performLogin()
        }

        @IBAction private func register(_ sender: Any) {
            presenter.performRegister()
        }

    }

    // MARK: - Configuration
    extension LoginViewController: LoginViewConfigurable {
        // UIKit is not allowed to be referenced in the Presenter. We expose the value of our outlets by using abstraction.
        var rememberMeIsOn: Bool { rememberMeSwitch.isOn }

        var usernameText: String? { usernameTextField.text }

        var passwordText: String? { passwordTextField.text }

        func setRememberMeSwitch(isOn: Bool) {
            rememberMeSwitch.isOn = isOn
        }

        func setUsername(_ username: String?) {
            usernameTextField.text = username
        }
    }

    // MARK: - Loadable
    extension LoginViewController: Loadable {}

    // MARK: - Error Handler
    extension LoginViewController: ErrorHandler {}

Loadable is the same helper protocol that we used in our previous versions of the codebase. It simply shows and hides a loading view, which comes in handy when doing some Network requests. It has a default implementation for classes of type UIView and UIViewController (example: extension Loadable where Self: UIViewController).

ErrorHandler is a new helper protocol that has one method:


    protocol ErrorHandler {
        func handleError(title: String, message: String)
    }

    extension ErrorHandler where Self: UIViewController {
        func handleError(title: String, message: String) {
            AlertHelper.present(in: self, title: title, message: message)
        }
    }

The default implementation uses the static method from AlertHelper to present an alert controller. We use it for displaying the Network errors.

We continue with the Presenter layer below:


    final class LoginPresenter: LoginPresentable {

        // MARK: - Properties
        weak var view: LoginViewProtocol?
        var interactor: LoginInteractorProtocol
        var router: LoginRouterProtocol

        // MARK: - Public API
        init(view: LoginViewProtocol? = nil,
             interactor: LoginInteractorProtocol = LoginInteractor(),
             router: LoginRouterProtocol = LoginRouter()) {
            self.view = view
            self.interactor = interactor
            self.router = router
        }

    }

We set our dependencies to be injected via the initialiser. Now, the presenter has two new dependencies: Interactor and Router.

After our ViewController finished to load the view, we notify the Presenter. We want to make the View more passive, so we let the Presenter to specify the View how to configure its UI elements with the information that we get from the Interactor:


    // MARK: - View Configuration
    extension LoginPresenter: LoginPresenterViewConfiguration {
        func viewDidLoad() {
            // Fetch the UserDefaults and Keychain values by asking the Interactor. Configure the UI elements based on the values we got.
            let rememberUsername = interactor.rememberUsername

            view?.setRememberMeSwitch(isOn: rememberUsername)

            if rememberUsername {
                view?.setUsername(interactor.username)
            }
        }
    }

The service API calls to login and register are similar:


    extension LoginPresenter: LoginPresenterServiceInteractable {
        func performLogin() {
            guard validateCredentials() else { return }

            view?.showLoadingView()

            interactor.login(username: username!, password: password!)
        }

        func performRegister() {
            guard validateCredentials() else { return }

            view?.showLoadingView()

            interactor.register(username: username!, password: password!)
        }

        private func validateCredentials() -> Bool {
            guard credentialsAreValid else {
                view?.handleError(title: "Error", message: "Both fields are mandatory.")
                return false
            }

            return true
        }

        private var credentialsAreValid: Bool {
            username?.isEmpty == false && password?.isEmpty == false
        }

        private var username: String? {
            view?.usernameText
        }

        private var password: String? {
            view?.passwordText
        }
    }

When the API calls are finished, the Interactor calls the following methods from the Presenter:


    // MARK: - Service Handler
    extension LoginPresenter: LoginPresenterServiceHandler {
        func serviceFailedWithError(_ error: Error) {
            view?.hideLoadingView()
            view?.handleError(title: "Error", message: String(describing: error))
        }

        func didLogin() {
            handleAuthCompletion()
        }

        func didRegister() {
            handleAuthCompletion()
        }

        private func handleAuthCompletion() {
            storeUsernameAndRememberMe()
            view?.hideLoadingView()
            router.showPlayerList()
        }

        private func storeUsernameAndRememberMe() {
            let rememberMe = view?.rememberMeIsOn ?? true

            if rememberMe {
                interactor.setUsername(view?.usernameText)
            } else {
                interactor.setUsername(nil)
            }
        }
    }

The Interactor now holds the business logic:


    final class LoginInteractor: LoginInteractable {

        weak var presenter: LoginPresenterProtocol?

        private let loginService: LoginService
        private let usersService: StandardNetworkService
        private let userDefaults: FootballGatherUserDefaults
        private let keychain: FootbalGatherKeychain

        init(loginService: LoginService = LoginService(),
             usersService: StandardNetworkService = StandardNetworkService(resourcePath: "/api/users"),
             userDefaults: FootballGatherUserDefaults = .shared,
             keychain: FootbalGatherKeychain = .shared) {
            self.loginService = loginService
            self.usersService = usersService
            self.userDefaults = userDefaults
            self.keychain = keychain
        }

    }

We expose in our Public API the actual values for rememberMe and the username:


    // MARK: - Credentials handler
    extension LoginInteractor: LoginInteractorCredentialsHandler {

        var rememberUsername: Bool { userDefaults.rememberUsername ?? true }

        var username: String? { keychain.username }

        func setRememberUsername(_ value: Bool) {
            userDefaults.rememberUsername = value
        }

        func setUsername(_ username: String?) {
            keychain.username = username
        }
    }

The service handlers are lighter than in previous architecture patterns:


    // MARK: - Services
    extension LoginInteractor: LoginInteractorServiceRequester {
        func login(username: String, password: String) {
            let requestModel = UserRequestModel(username: username, password: password)
            loginService.login(user: requestModel) { [weak self] result in
                DispatchQueue.main.async {
                    switch result {
                    case .failure(let error):
                        self?.presenter?.serviceFailedWithError(error)

                    case .success(_):
                        self?.presenter?.didLogin()
                    }
                }
            }
        }

        func register(username: String, password: String) {
            guard let hashedPasssword = Crypto.hash(message: password) else {
                fatalError("Unable to hash password")
            }

            let requestModel = UserRequestModel(username: username, password: hashedPasssword)
            usersService.create(requestModel) { [weak self] result in
                DispatchQueue.main.async {
                    switch result {
                    case .failure(let error):
                        self?.presenter?.serviceFailedWithError(error)

                    case .success(let resourceId):
                        print("Created user: \(resourceId)")
                        self?.presenter?.didRegister()
                    }
                }
            }
        }
    }

When editing a player, we use delegation for refreshing the list of the players from the PlayerList module.


    struct ModuleFactory: ModuleFactoryProtocol {
        func makePlayerDetails(using navigationController: UINavigationController = UINavigationController(),
                               for player: PlayerResponseModel,
                               delegate: PlayerDetailDelegate) -> PlayerDetailModule {
            let router = PlayerDetailRouter(navigationController: navigationController, moduleFactory: self)
            let view: PlayerDetailViewController = Storyboard.defaultStoryboard.instantiateViewController()
            let interactor = PlayerDetailInteractor(player: player)
            let presenter = PlayerDetailPresenter(interactor: interactor, delegate: delegate)

            return PlayerDetailModule(view: view, router: router, interactor: interactor, presenter: presenter)
        }

        func makePlayerEdit(using navigationController: UINavigationController = UINavigationController(),
                            for playerEditable: PlayerEditable,
                            delegate: PlayerEditDelegate) -> PlayerEditModule {
            let router = PlayerEditRouter(navigationController: navigationController, moduleFactory: self)
            let view: PlayerEditViewController = Storyboard.defaultStoryboard.instantiateViewController()
            let interactor = PlayerEditInteractor(playerEditable: playerEditable)
            let presenter = PlayerEditPresenter(interactor: interactor, delegate: delegate)

            return PlayerEditModule(view: view, router: router, interactor: interactor, presenter: presenter)
        }
    ā€¦
    }

Navigating to Edit screen

We show PlayerDetailsView by calling the router from PlayerListPresenter:


    func selectRow(at index: Int) {
       guard playersCollectionIsEmpty == false else {
           return
       }

       if isInListViewMode {
           let player = interactor.players[index]
           showDetailsView(for: player)
       } else {
           toggleRow(at: index)
           updateSelectedRows(at: index)
           reloadViewAfterRowSelection(at: index)
       }
    }

    private func showDetailsView(for player: PlayerResponseModel) {
       router.showDetails(for: player, delegate: self)
    }

PlayerListRouter is shown below:


    extension PlayerListRouter: PlayerListRouterProtocol {
        func showDetails(for player: PlayerResponseModel, delegate: PlayerDetailDelegate) {
            let module = moduleFactory.makePlayerDetails(using: navigationController, for: player, delegate: delegate)

            if let viewController = module.assemble() {
                navigationController.pushViewController(viewController, animated: true)
            }
        }
    }

Now, we use the same approach from Detail screen to Edit screen:


    func selectRow(at indexPath: IndexPath) {
       let player = interactor.player
       let rowDetails = sections[indexPath.section].rows[indexPath.row]
       let items = self.items(for: rowDetails.editableField)
       let selectedItemIndex = items.firstIndex(of: rowDetails.value.lowercased())
       let editablePlayerDetails = PlayerEditable(player: player,
                                                  items: items,
                                                  selectedItemIndex: selectedItemIndex,
                                                  rowDetails: rowDetails)

       router.showEditView(with: editablePlayerDetails, delegate: self)
    }

And the router:


    extension PlayerDetailRouter: PlayerDetailRouterProtocol {
        func showEditView(with editablePlayerDetails: PlayerEditable, delegate: PlayerEditDelegate) {
            let module = moduleFactory.makePlayerEdit(using: navigationController, for: editablePlayerDetails, delegate: delegate)

            if let viewController = module.assemble() {
                navigationController.pushViewController(viewController, animated: true)
            }
        }
    }

Navigating back to the List screen

When the user confirms the changes to a player, we call our presenter delegate.


    extension PlayerEditPresenter: PlayerEditPresenterServiceHandler {
        func playerWasUpdated() {
            view?.hideLoadingView()
            delegate?.didUpdate(player: interactor.playerEditable.player)
            router.dismissEditView()
        }
    }

The delegate is PlayerDetailsPresenter:


    // MARK: - PlayerEditDelegate
    extension PlayerDetailPresenter: PlayerEditDelegate {
        func didUpdate(player: PlayerResponseModel) {
            interactor.updatePlayer(player)
            delegate?.didUpdate(player: player)
        }
    }

Finally, we call the PlayerDetailDelegate (assigned to PlayerListPresenter) and refresh the list of players:


    // MARK: - PlayerEditDelegate
    extension PlayerListPresenter: PlayerDetailDelegate {
        func didUpdate(player: PlayerResponseModel) {
            interactor.updatePlayer(player)
        }
    }

We follow the same approach for Confirm and Add modules:


    func confirmOrAddPlayers() {
       if isInListViewMode {
           showAddPlayerView()
       } else {
           showConfirmPlayersView()
       }
    }

    private var isInListViewMode: Bool {
       viewState == .list
    }

    private func showAddPlayerView() {
       router.showAddPlayer(delegate: self)
    }

    private func showConfirmPlayersView() {
       router.showConfirmPlayers(with: interactor.selectedPlayers(atRows: selectedRows), delegate: self)
    }

The Router class is presented below:


    extension PlayerListRouter: PlayerListRouterProtocol {
        func showAddPlayer(delegate: PlayerAddDelegate) {
            let module = moduleFactory.makePlayerAdd(using: navigationController, delegate: delegate)

            if let viewController = module.assemble() {
                navigationController.pushViewController(viewController, animated: true)
            }
        }

        func showConfirmPlayers(with playersDictionary: [TeamSection: [PlayerResponseModel]], delegate: ConfirmPlayersDelegate) {
            let module = moduleFactory.makeConfirmPlayers(using: navigationController, playersDictionary: playersDictionary, delegate: delegate)

            if let viewController = module.assemble() {
                navigationController.pushViewController(viewController, animated: true)
            }
        }
    }

Implementing the service handler in PlayerAddPresenter:


    extension PlayerAddPresenter: PlayerAddPresenterServiceHandler {
        func playerWasAdded() {
            view?.hideLoadingView()
            delegate?.didAddPlayer()
            router.dismissAddView()
        }
    }

Finally, delegation to the list of players:


    // MARK: - PlayerAddDelegate
    extension PlayerListPresenter: PlayerAddDelegate {
        func didAddPlayer() {
            loadPlayers()
        }
    }

    // MARK: - ConfirmPlayersDelegate
    extension PlayerListPresenter: ConfirmPlayersDelegate {
        func didEndGather() {
            viewState = .list
            configureView()
            view?.reloadData()
        }
    }

In this architecture pattern, we wanted to make the View as passive as we could (this concept should be applied to MVP, too).
For that we created for the table rows, a CellViewPresenter:


    protocol PlayerTableViewCellPresenterProtocol: AnyObject {
        var view: PlayerTableViewCellProtocol? { get set }
        var viewState: PlayerListViewState { get set }
        var isSelected: Bool { get set }

        func setupView()
        func configure(with player: PlayerResponseModel)
        func toggle()
    }

The concrete class described below:


    final class PlayerTableViewCellPresenter: PlayerTableViewCellPresenterProtocol {

        var view: PlayerTableViewCellProtocol?
        var viewState: PlayerListViewState
        var isSelected = false

        init(view: PlayerTableViewCellProtocol? = nil,
             viewState: PlayerListViewState = .list) {
            self.view = view
            self.viewState = viewState
        }

        func setupView() {
            if viewState == .list {
                view?.setupDefaultView()
            } else {
                view?.setupViewForSelection(isSelected: isSelected)
            }
        }

        func toggle() {
            isSelected.toggle()

            if viewState == .selection {
                view?.setupCheckBoxImage(isSelected: isSelected)
            }
        }

        func configure(with player: PlayerResponseModel) {
            view?.set(nameDescription: player.name)
            setPositionDescription(for: player)
            setSkillDescription(for: player)
        }

        private func setPositionDescription(for player: PlayerResponseModel) {
            let position = player.preferredPosition?.rawValue
            view?.set(positionDescription: "Position: \(position ?? "-")")
        }

        private func setSkillDescription(for player: PlayerResponseModel) {
            let skill = player.skill?.rawValue
            view?.set(skillDescription: "Skill: \(skill ?? "-")")
        }

    }

The presenter will update the CellView:


    final class PlayerTableViewCell: UITableViewCell, PlayerTableViewCellProtocol {
        @IBOutlet weak var checkboxImageView: UIImageView!
        @IBOutlet weak var playerCellLeftConstraint: NSLayoutConstraint!
        @IBOutlet weak var nameLabel: UILabel!
        @IBOutlet weak var positionLabel: UILabel!
        @IBOutlet weak var skillLabel: UILabel!

        private enum Constants {
            static let playerContentLeftPadding: CGFloat = 10.0
            static let playerContentAndIconLeftPadding: CGFloat = -20.0
        }

        func setupDefaultView() {
            playerCellLeftConstraint.constant = Constants.playerContentAndIconLeftPadding
            setupCheckBoxImage(isSelected: false)
            checkboxImageView.isHidden = true
        }

        func setupViewForSelection(isSelected: Bool) {
            playerCellLeftConstraint.constant = Constants.playerContentLeftPadding
            checkboxImageView.isHidden = false
            setupCheckBoxImage(isSelected: isSelected)
        }

        func setupCheckBoxImage(isSelected: Bool) {
            let imageName = isSelected ? "ticked" : "unticked"
            checkboxImageView.image = UIImage(named: imageName)
        }

        func set(nameDescription: String) {
            nameLabel.text = nameDescription
        }

        func set(positionDescription: String) {
            positionLabel.text = positionDescription
        }

        func set(skillDescription: String) {
            skillLabel.text = skillDescription
        }

    }

In PlayerViewController, we have the cellForRowAt method:


    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       guard let cell: PlayerTableViewCell = tableView.dequeueReusableCell(withIdentifier: "PlayerTableViewCell") as? PlayerTableViewCell else {
           return UITableViewCell()
       }

       let index = indexPath.row
       let cellPresenter = presenter.cellPresenter(at: index)
       let player = presenter.player(at: index)

       cellPresenter.view = cell
       cellPresenter.setupView()
       cellPresenter.configure(with: player)

       return cell
    }

Inside the Presenter we cache the existing cell presenters:


    func cellPresenter(at index: Int) -> PlayerTableViewCellPresenterProtocol {
       if let cellPresenter = cellPresenters[index] {
           cellPresenter.viewState = viewState
           return cellPresenter
       }

       let cellPresenter = PlayerTableViewCellPresenter(viewState: viewState)
       cellPresenters[index] = cellPresenter

       return cellPresenter
    }

Finally, we present our main app module, Gather.

GatherViewController is simplified and looks great:


    // MARK: - GatherViewController
    final class GatherViewController: UIViewController, GatherViewable {

        // MARK: - Properties
        @IBOutlet weak var playerTableView: UITableView!
        @IBOutlet weak var scoreLabelView: ScoreLabelView!
        @IBOutlet weak var scoreStepper: ScoreStepper!
        @IBOutlet weak var timerLabel: UILabel!
        @IBOutlet weak var timerView: UIView!
        @IBOutlet weak var timePickerView: UIPickerView!
        @IBOutlet weak var actionTimerButton: UIButton!

        lazy var loadingView = LoadingView.initToView(view)

        var presenter: GatherPresenterProtocol!

        // MARK: - View life cycle
        override func viewDidLoad() {
            super.viewDidLoad()
            presenter.viewDidLoad()
        }

        // MARK: - IBActions
        @IBAction private func endGather(_ sender: Any) {
            presenter.requestToEndGather()
        }

        @IBAction private func setTimer(_ sender: Any) {
            presenter.setTimer()
        }

        @IBAction private func cancelTimer(_ sender: Any) {
            presenter.cancelTimer()
        }

        @IBAction private func actionTimer(_ sender: Any) {
            presenter.actionTimer()
        }

        @IBAction private func timerCancel(_ sender: Any) {
            presenter.timerCancel()
        }

        @IBAction private func timerDone(_ sender: Any) {
            presenter.timerDone()
        }

    }

We expose the Public API using the protocol GatherViewConfigurable:


    // MARK: - Configuration
    extension GatherViewController: GatherViewConfigurable {
        var scoreDescription: String {
            scoreLabelView.scoreDescription
        }

        var winnerTeamDescription: String {
            scoreLabelView.winnerTeamDescription
        }

        func configureTitle(_ title: String) {
            self.title = title
        }

        func setActionButtonTitle(_ title: String) {
            actionTimerButton.setTitle(title, for: .normal)
        }

        func setupScoreStepper() {
            scoreStepper.delegate = self
        }

        func setTimerViewVisibility(isHidden: Bool) {
            timerView.isHidden = isHidden
        }

        func selectRow(_ row: Int, inComponent component: Int, animated: Bool = false) {
            timePickerView.selectRow(row, inComponent: component, animated: animated)
        }

        func selectedRow(in component: Int) -> Int {
            timePickerView.selectedRow(inComponent: component)
        }

        func setTimerLabelText(_ text: String) {
            timerLabel.text = text
        }

        func setTeamALabelText(_ text: String) {
            scoreLabelView.teamAScoreLabel.text = text
        }

        func setTeamBLabelText(_ text: String) {
            scoreLabelView.teamBScoreLabel.text = text
        }
    }

GatherViewReloadable defines the reloadData method. Here, we reload all picker components and the tableView data.


    // MARK: - Reload
    extension GatherViewController: GatherViewReloadable {
        func reloadData() {
            timePickerView.reloadAllComponents()
            playerTableView.reloadData()
        }
    }

We donā€™t have any more two separate layers, ViewController and View. The alert controller presentation is done inside the View layer:


    // MARK: - Confirmation
    extension GatherViewController: GatherViewConfirmable {
        func displayConfirmationAlert() {
            let alertController = UIAlertController(title: "End Gather", message: "Are you sure you want to end the gather?", preferredStyle: .alert)
            let confirmAction = UIAlertAction(title: "Yes", style: .default) { [weak self] _ in
                self?.presenter.endGather()
            }
            alertController.addAction(confirmAction)

            let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
            alertController.addAction(cancelAction)

            present(alertController, animated: true, completion: nil)
        }
    }

We could have used a separate layer and create another object for the tableā€™s and pickerā€™s DataSource and Delegate, but for the sake of our exercise we preferred to implement the methods inside our ViewController:


    // MARK: - UITableViewDelegate | UITableViewDataSource
    extension GatherViewController: UITableViewDelegate, UITableViewDataSource {
        func numberOfSections(in tableView: UITableView) -> Int {
            presenter.numberOfSections
        }

        func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
            presenter.titleForHeaderInSection(section)
        }

        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            presenter.numberOfRowsInSection(section)
        }

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "GatherCellId") else {
                return UITableViewCell()
            }

            let cellPresenter = GatherTableViewCellPresenter(view: cell)
            cellPresenter.configure(title: presenter.rowTitle(at: indexPath), descriptionDetails: presenter.rowDescription(at: indexPath))

            return cell
        }
    }

    // MARK: - UIPickerViewDataSource
    extension GatherViewController: UIPickerViewDataSource, UIPickerViewDelegate {
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            presenter.numberOfPickerComponents
        }

        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            presenter.numberOfRowsInPickerComponent(component)
        }

        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            presenter.titleForPickerRow(row, forComponent: component)
        }
    }

We also have implemented the ScoreStepperDelegate to pass to the presenter the UI updates of the team sliders.

And finally some helper protocols to add functionality for our custom cell, showing and hiding a loading spinner and to handle errors.


    // MARK: - UITableViewCell
    extension UITableViewCell: GatherTableViewCellProtocol {}

    // MARK: - Loadable
    extension GatherViewController: Loadable {}

    // MARK: - Error Handler
    extension GatherViewController: ErrorHandler {}

Testing our business logic

In VIPER we have the Interactor handling our business logic. This should be tested.

However, the core of the architecture is the Presenter, which handles updates to the View and communicates with both the Router and Interactor. This should be tested as well.

Testing the Presenter

The class for for unit testing the presenter is GatherPresenterTests:


    final class GatherPresenterTests: XCTestCase {

        // MARK: - GatherPresenterViewConfiguration
        func testViewDidLoad_whenPresenterIsAllocated_configuresView() {
            // given
            let mockView = GatherMockView()
            let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
            let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

            // when
            sut.viewDidLoad()

            // then
            XCTAssertEqual(mockView.title, "Gather in progress")
            XCTAssertTrue(mockView.timerViewIsHidden)
            XCTAssertEqual(mockView.selectionDictionary[mockInteractor.minutesComponent!.rawValue], mockInteractor.selectedTime.minutes)
            XCTAssertEqual(mockView.selectionDictionary[mockInteractor.secondsComponent!.rawValue], mockInteractor.selectedTime.seconds)
            XCTAssertEqual(mockView.timerLabelText, "10:00")
            XCTAssertEqual(mockView.actionButtonTitle, "Start")
            XCTAssertTrue(mockView.scoreStepperWasSetup)
            XCTAssertTrue(mockView.viewWasReloaded)
        }

        func testViewDidLoad_whenTimeComponentsAreEmpty_minutesComponentIsNil() {
            // given
            let mockView = GatherMockView()
            let mockGather = GatherModel(players: [], gatherUUID: UUID())
            let mockInteractor = GatherInteractor(gather: mockGather, timeComponents: [])
            let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

            // when
            sut.viewDidLoad()

            // then
            XCTAssertNil(mockView.selectionDictionary[0])
            XCTAssertNil(mockView.selectionDictionary[1])
        }

    }

Testing the table view's and picker view's data source:


    // MARK: - Table Data Source
    func testNumberOfSections_whenPresenterIsAllocated_equalsTeamSectionsCount() {
       // given
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let numberOfSections = sut.numberOfSections

       // then
       XCTAssertEqual(mockInteractor.teamSections.count, numberOfSections)
    }

    func testNumberOfRowsInSection_whenSectionIsZero_equalsNumberOfPlayersInTeamSection() {
       // given
       let mockGather = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)
       let expectedNumberOfPlayers = mockGather.players.filter { $0.team == .teamA }.count
       let mockInteractor = GatherInteractor(gather: mockGather)
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let numberOfRowsInSection = sut.numberOfRowsInSection(0)

       // then
       XCTAssertEqual(numberOfRowsInSection, expectedNumberOfPlayers)
    }

    func testNumberOfRowsInSection_whenSectionIsOne_equalsNumberOfPlayersInTeamSection() {
       // given
       let mockGather = ModelsMockFactory.makeGatherModel(numberOfPlayers: 5)
       let expectedNumberOfPlayers = mockGather.players.filter { $0.team == .teamB }.count
       let mockInteractor = GatherInteractor(gather: mockGather)
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let numberOfRowsInSection = sut.numberOfRowsInSection(1)

       // then
       XCTAssertEqual(numberOfRowsInSection, expectedNumberOfPlayers)
    }

    func testRowTitle_whenInteractorHasPlayers_equalsPlayerName() {
       // given
       let mockGather = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)
       let expectedRowTitle = mockGather.players.filter { $0.team == .teamA }.first?.player.name
       let mockInteractor = GatherInteractor(gather: mockGather)
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let rowTitle = sut.rowTitle(at: IndexPath(row: 0, section: 0))

       // then
       XCTAssertEqual(rowTitle, expectedRowTitle)
    }

    func testRowDescription_whenInteractorHasPlayers_equalsPlayerPreferredPositionAcronym() {
       // given
       let mockGather = ModelsMockFactory.makeGatherModel(numberOfPlayers: 1)
       let expectedRowDescription = mockGather.players.filter { $0.team == .teamB }.first?.player.preferredPosition?.acronym
       let mockInteractor = GatherInteractor(gather: mockGather)
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let rowDescription = sut.rowDescription(at: IndexPath(row: 0, section: 1))

       // then
       XCTAssertEqual(rowDescription, expectedRowDescription)
    }

    func testTitleForHeaderInSection_whenSectionIsTeamA_equalsTeamSectionHeaderTitle() {
       // given
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let expectedTitle = TeamSection.teamA.headerTitle
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let titleForHeader = sut.titleForHeaderInSection(0)

       // then
       XCTAssertEqual(titleForHeader, expectedTitle)
    }

    func testTitleForHeaderInSection_whenSectionIsTeamB_equalsTeamSectionHeaderTitle() {
       // given
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let expectedTitle = TeamSection.teamB.headerTitle
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let titleForHeader = sut.titleForHeaderInSection(1)

       // then
       XCTAssertEqual(titleForHeader, expectedTitle)
    }

    // MARK: - Picker Data Source
    func testNumberOfPickerComponents_whenTimeComponentsAreGiven_equalsInteractorTimeComponents() {
       // given
       let mockTimeComponents: [GatherTimeHandler.Component] = [.minutes]
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()), timeComponents: mockTimeComponents)
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let numberOfPickerComponents = sut.numberOfPickerComponents

       // then
       XCTAssertEqual(numberOfPickerComponents, mockTimeComponents.count)
    }

    func testNumberOfRowsInPickerComponent_whenComponentIsMinutes_equalsNumberOfSteps() {
       // given
       let mockTimeComponents: [GatherTimeHandler.Component] = [.minutes]
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()), timeComponents: mockTimeComponents)
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let numberOfRowsInPickerComponent = sut.numberOfRowsInPickerComponent(0)

       // then
       XCTAssertEqual(numberOfRowsInPickerComponent, GatherTimeHandler.Component.minutes.numberOfSteps)
    }

    func testNumberOfRowsInPickerComponent_whenComponentIsSeconds_equalsNumberOfSteps() {
       // given
       let mockTimeComponents: [GatherTimeHandler.Component] = [.seconds]
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()), timeComponents: mockTimeComponents)
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let numberOfRowsInPickerComponent = sut.numberOfRowsInPickerComponent(0)

       // then
       XCTAssertEqual(numberOfRowsInPickerComponent, GatherTimeHandler.Component.seconds.numberOfSteps)
    }

    func testTitleForPickerRow_whenComponentsAreNotEmpty_containsTimeComponentShort() {
       // given
       let mockTimeComponents: [GatherTimeHandler.Component] = [.seconds]
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()), timeComponents: mockTimeComponents)
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       let titleForPickerRow = sut.titleForPickerRow(0, forComponent: 0)

       // then
       XCTAssertTrue(titleForPickerRow.contains(GatherTimeHandler.Component.seconds.short))
    }

Testing the stepper handler:


    // MARK: - Stepper Handler
    func testUpdateValue_whenTeamIsA_viewSetsTeamALabelTextWithNewValue() {
       // given
       let mockValue = 15.0
       let mockView = GatherMockView()
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.updateValue(for: .teamA, with: mockValue)

       // then
       XCTAssertEqual(mockView.teamALabelText, "\(Int(mockValue))")
    }

    func testUpdateValue_whenTeamIsB_viewSetsTeamBLabelTextWithNewValue() {
       // given
       let mockValue = 15.0
       let mockView = GatherMockView()
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.updateValue(for: .teamB, with: mockValue)

       // then
       XCTAssertEqual(mockView.teamBLabelText, "\(Int(mockValue))")
    }

Testing the IBActions:


    // MARK: - Actions
    func testRequestToEndGather_whenPresenterIsAllocated_viewDisplaysConfirmationAlert() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.requestToEndGather()

       // then
       XCTAssertTrue(mockView.confirmationAlertWasDisplayed)
    }

    func testSetTimer_whenPresenterIsAllocated_selectsRowAndSetsTimerViewVisibile() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.setTimer()

       // then
       XCTAssertNotNil(mockView.selectionDictionary[0])
       XCTAssertNotNil(mockView.selectionDictionary[1])
       XCTAssertFalse(mockView.timerViewIsHidden)
    }

    func testCancelTimer_whenPresenterIsAllocated_resetsTimerAndUpdatesView() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherMockInteractor()
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.cancelTimer()

       // then
       XCTAssertTrue(mockInteractor.timerWasStopped)
       XCTAssertTrue(mockInteractor.timerWasResetted)
       XCTAssertNotNil(mockView.timerLabelText)
       XCTAssertNotNil(mockView.actionButtonTitle)
       XCTAssertTrue(mockView.timerViewIsHidden)
    }

    func testActionTimer_whenPresenterIsAllocated_togglesTimerAndUpdatesActionButtonTitle() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherMockInteractor()
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.actionTimer()

       // then
       XCTAssertTrue(mockInteractor.timerWasToggled)
       XCTAssertNotNil(mockView.actionButtonTitle)
    }

    func testTimerCancel_whenPresenterIsAllocated_timerViewIsHidden() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.timerCancel()

       // then
       XCTAssertTrue(mockView.timerViewIsHidden)
    }

    func testTimerDone_whenPresenterIsAllocated_stopsTimerUpdatesTimeAndConfiguresView() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherMockInteractor()
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.timerDone()

       // then
       XCTAssertTrue(mockInteractor.timerWasStopped)
       XCTAssertTrue(mockInteractor.timeWasUpdated)
       XCTAssertNotNil(mockView.timerLabelText)
       XCTAssertNotNil(mockView.actionButtonTitle)
       XCTAssertTrue(mockView.timerViewIsHidden)
    }

    func testTimerDone_whenViewIsNil_stopsTimerUpdatesTimeAndConfiguresView() {
       // given
       let mockInteractor = GatherMockInteractor()
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       sut.timerDone()

       // then
       XCTAssertFalse(mockInteractor.timeWasUpdated)
    }

    func testEndGather_whenViewIsNotNil_showsLoadingViewAndEndsGather() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherMockInteractor()
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.endGather()

       // then
       XCTAssertTrue(mockView.loadingViewWasShown)
       XCTAssertTrue(mockInteractor.gatherWasEnded)
    }

    func testEndGather_whenViewIsNil_returns() {
       // given
       let mockInteractor = GatherMockInteractor()
       let sut = GatherPresenter(interactor: mockInteractor)

       // when
       sut.endGather()

       // then
       XCTAssertFalse(mockInteractor.gatherWasEnded)
    }

    func testGatherEnded_whenPresenterIsAllocated_hidesLoadingViewEndGathersAndPopsToPlayerList() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherMockInteractor()
       let mockDelegate = GatherMockDelegate()
       let mockRouter = GatherMockRouter()
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor, router: mockRouter, delegate: mockDelegate)

       // when
       sut.gatherEnded()

       // then
       XCTAssertTrue(mockView.loadingViewWasHidden)
       XCTAssertTrue(mockDelegate.gatherWasEnded)
       XCTAssertTrue(mockRouter.poppedToPlayerList)
    }

    func testServiceFailedToEndGather_whenPresenterIsAllocated_hidesLoadingViewAndHandlesError() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.serviceFailedToEndGather()

       // then
       XCTAssertTrue(mockView.loadingViewWasHidden)
       XCTAssertTrue(mockView.errorWasHandled)
    }

    func testTimerDecremented_whenPresenterIsAllocated_setsTimerLabelText() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.timerDecremented()

       // then
       XCTAssertNotNil(mockView.timerLabelText)
    }

    func testActionButtonTitle_whenTimerStateIsPaused_isResume() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherMockInteractor(timerState: .paused)
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.actionTimer()

       // then
       XCTAssertEqual(mockView.actionButtonTitle, "Resume")
    }

    func testActionButtonTitle_whenTimerStateIsRunning_isPause() {
       // given
       let mockView = GatherMockView()
       let mockInteractor = GatherMockInteractor(timerState: .running)
       let sut = GatherPresenter(view: mockView, interactor: mockInteractor)

       // when
       sut.actionTimer()

       // then
       XCTAssertEqual(mockView.actionButtonTitle, "Pause")
    }

The mocks are defined in a separate file:


    // MARK: - View
    final class GatherMockView: GatherViewProtocol {
        var presenter: GatherPresenterProtocol!
        var loadingView = LoadingView()

        private(set) var title: String?
        private(set) var timerViewIsHidden = false
        private(set) var selectionDictionary: [Int: Int] = [:]
        private(set) var timerLabelText: String?
        private(set) var actionButtonTitle: String?
        private(set) var scoreStepperWasSetup = false
        private(set) var viewWasReloaded = false
        private(set) var teamALabelText: String?
        private(set) var teamBLabelText: String?
        private(set) var confirmationAlertWasDisplayed = false
        private(set) var loadingViewWasShown = false
        private(set) var loadingViewWasHidden = false
        private(set) var errorWasHandled = false

        var scoreDescription: String { "" }

        var winnerTeamDescription: String { "" }

        func configureTitle(_ title: String) {
            self.title = title
        }

        func setTimerViewVisibility(isHidden: Bool) {
            timerViewIsHidden = isHidden
        }

        func selectRow(_ row: Int, inComponent component: Int, animated: Bool) {
            selectionDictionary[component] = row
        }

        func setTimerLabelText(_ text: String) {
            timerLabelText = text
        }

        func setActionButtonTitle(_ title: String) {
            actionButtonTitle = title
        }

        func setupScoreStepper() {
            scoreStepperWasSetup = true
        }

        func reloadData() {
            viewWasReloaded = true
        }

        func setTeamALabelText(_ text: String) {
            teamALabelText = text
        }

        func setTeamBLabelText(_ text: String) {
            teamBLabelText = text
        }

        func displayConfirmationAlert() {
            confirmationAlertWasDisplayed = true
        }

        func showLoadingView() {
            loadingViewWasShown = true
        }

        func hideLoadingView() {
            loadingViewWasHidden = true
        }

        func handleError(title: String, message: String) {
            errorWasHandled = true
        }

        func selectedRow(in component: Int) -> Int { 0 }
    }

    // MARK: - Interactor
    final class GatherMockInteractor: GatherInteractorProtocol {

        var presenter: GatherPresenterServiceHandler?
        var teamSections: [TeamSection] = TeamSection.allCases

        private(set) var timerState: GatherTimeHandler.State
        private(set) var timerWasStopped = false
        private(set) var timerWasResetted = false
        private(set) var timerWasToggled = false
        private(set) var timeWasUpdated = false
        private(set) var gatherWasEnded = false

        init(timerState: GatherTimeHandler.State = .stopped) {
            self.timerState = timerState
        }

        func stopTimer() {
            timerWasStopped = true
        }

        func resetTimer() {
            timerWasResetted = true
        }

        func teamSection(at index: Int) -> TeamSection {
            teamSections[index]
        }

        func toggleTimer() {
            timerWasToggled = true
        }

        func updateTime(_ gatherTime: GatherTime) {
            timeWasUpdated = true
        }

        func endGather(score: String, winnerTeam: String) {
            gatherWasEnded = true
        }

        var selectedTime: GatherTime { .defaultTime }

        var minutesComponent: GatherTimeHandler.Component? { .minutes }

        var secondsComponent: GatherTimeHandler.Component? { .seconds }

        var timeComponents: [GatherTimeHandler.Component] = GatherTimeHandler.Component.allCases

        func timeComponent(at index: Int) -> GatherTimeHandler.Component {
            timeComponents[index]
        }

        func players(in team: TeamSection) -> [PlayerResponseModel] { [] }

    }

    // MARK: - Delegate
    final class GatherMockDelegate: GatherDelegate {
        private(set) var gatherWasEnded = false

        func didEndGather() {
            gatherWasEnded = true
        }

    }

    // MARK: - Router
    final class GatherMockRouter: GatherRouterProtocol {
        private(set) var poppedToPlayerList = false

        func popToPlayerListView() {
            poppedToPlayerList = true
        }

    }

Testing the Interactor:


    import XCTest
    @testable import FootballGather

    final class GatherInteractorTests: XCTestCase {

        func testTeamSections_whenInteractorIsAllocated_equalsTeamAandTeamB() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let teamSections = sut.teamSections

            // then
            XCTAssertEqual(teamSections, [.teamA, .teamB])
        }

        func testTeamSection_whenIndexIsZero_equalsTeamA() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let teamSection = sut.teamSection(at: 0)

            // then
            XCTAssertEqual(teamSection, .teamA)
        }

        func testTeamSection_whenIndexIsOne_equalsTeamB() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let teamSection = sut.teamSection(at: 1)

            // then
            XCTAssertEqual(teamSection, .teamB)
        }

        func testPlayersInTeam_whenInteractorHasPlayers_returnsPlayersForTheGivenTeam() {
            // given
            let mockGather = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)
            let expectedPlayers = mockGather.players.filter { $0.team == .teamA }.compactMap { $0.player }
            let sut = GatherInteractor(gather: mockGather)

            // when
            let players = sut.players(in: .teamA)

            // then
            XCTAssertEqual(players, expectedPlayers)
        }

        func testEndGather_whenScoreIsSet_updatesGather() {
            // given
            let appKeychain = AppKeychainMockFactory.makeKeychain()
            appKeychain.token = ModelsMock.token
            let session = URLSessionMockFactory.makeSession()

            let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)
            let mockEndpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: "/api/gathers")
            let mockService = StandardNetworkService(session: session, urlRequest: AuthURLRequestFactory(endpoint: mockEndpoint, keychain: appKeychain))
            let mockPresenter = GatherMockPresenter()
            let exp = expectation(description: "Update gather expectation")
            mockPresenter.expectation = exp
            let sut = GatherInteractor(gather: mockGatherModel, updateGatherService: mockService)
            sut.presenter = mockPresenter

            // when
            sut.endGather(score: "1-1", winnerTeam: "None")

            // then
            waitForExpectations(timeout: 5) { _ in
                XCTAssertTrue(mockPresenter.gatherEndedCalled)
                appKeychain.storage.removeAll()
            }
        }

        func testEndGather_whenScoreIsNotSet_updatesGather() {
            // given
            let appKeychain = AppKeychainMockFactory.makeKeychain()
            appKeychain.token = ModelsMock.token
            let session = URLSessionMockFactory.makeSession()

            let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)
            let mockEndpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: "/api/gathers")
            let mockService = StandardNetworkService(session: session, urlRequest: AuthURLRequestFactory(endpoint: mockEndpoint, keychain: appKeychain))
            let mockPresenter = GatherMockPresenter()
            let exp = expectation(description: "Update gather expectation")
            mockPresenter.expectation = exp
            let sut = GatherInteractor(gather: mockGatherModel, updateGatherService: mockService)
            sut.presenter = mockPresenter

            // when
            sut.endGather(score: "", winnerTeam: "")

            // then
            waitForExpectations(timeout: 5) { _ in
                XCTAssertTrue(mockPresenter.serviceFailedCalled)
                appKeychain.storage.removeAll()
            }
        }

        func testMinutesComponent_whenInteractorIsAllocated_isMinutes() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let minutesComponent = sut.minutesComponent

            // then
            XCTAssertEqual(minutesComponent, .minutes)
        }

        func testSecondsComponent_whenInteractorIsAllocated_isSeconds() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let secondsComponent = sut.secondsComponent

            // then
            XCTAssertEqual(secondsComponent, .seconds)
        }

        func testSelectedTime_whenInteractorIsAllocated_isDefaultTime() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let selectedTime = sut.selectedTime

            // then
            XCTAssertEqual(selectedTime.minutes, GatherTime.defaultTime.minutes)
            XCTAssertEqual(selectedTime.seconds, GatherTime.defaultTime.seconds)
        }

        func testTimerState_whenInteractorIsAllocated_isStopped() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let timerState = sut.timerState

            // then
            XCTAssertEqual(timerState, .stopped)
        }

        func testTimeComponent_whenIndexIsZero_isMinutes() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let timeComponent = sut.timeComponent(at: 0)

            // then
            XCTAssertEqual(timeComponent, .minutes)
        }

        func testTimeComponent_whenIndexIsOne_isSeconds() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            let timeComponent = sut.timeComponent(at: 1)

            // then
            XCTAssertEqual(timeComponent, .seconds)
        }

        func testStopTimer_whenStateIsRunning_isStopped() {
            // given
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()), timeHandler: GatherTimeHandler(state: .running))

            // when
            sut.stopTimer()

            // then
            XCTAssertEqual(sut.timerState, .stopped)
        }

        func testUpdateTimer_whenGatherTimeIsDifferent_updatesSelectedTime() {
            // given
            let mockSelectedTime = GatherTime(minutes: 99, seconds: 101)
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()))

            // when
            sut.updateTime(mockSelectedTime)

            // then
            XCTAssertEqual(sut.selectedTime.minutes, mockSelectedTime.minutes)
            XCTAssertEqual(sut.selectedTime.seconds, mockSelectedTime.seconds)
        }

        func testToggleTimer_whenTimeIsValid_decrementsTime() {
            // given
            let numberOfUpdateCalls = 2
            let mockSelectedTime = GatherTime(minutes: 0, seconds: numberOfUpdateCalls)
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()), timeHandler: GatherTimeHandler(selectedTime: mockSelectedTime))

            let mockPresenter = GatherMockPresenter()
            let exp = expectation(description: "Update gather expectation")
            mockPresenter.expectation = exp
            mockPresenter.numberOfUpdateCalls = numberOfUpdateCalls

            sut.presenter = mockPresenter

            // when
            sut.toggleTimer()

            // then
            waitForExpectations(timeout: 5) { _ in
                XCTAssertTrue(mockPresenter.timerDecrementedCalled)
                XCTAssertEqual(mockPresenter.actualUpdateCalls, numberOfUpdateCalls)
                sut.stopTimer()
            }
        }

        func testToggleTimer_whenTimeIsInvalid_returns() {
            // given
            let mockSelectedTime = GatherTime(minutes: -1, seconds: -1)
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()), timeHandler: GatherTimeHandler(selectedTime: mockSelectedTime))
            let mockPresenter = GatherMockPresenter()
            sut.presenter = mockPresenter

            // when
            sut.toggleTimer()

            // then
            XCTAssertFalse(mockPresenter.timerDecrementedCalled)
        }

        func testResetTimer_whenInteractorIsAllocated_resetsTime() {
            // given
            let numberOfUpdateCalls = 1
            let mockSelectedTime = GatherTime(minutes: 0, seconds: numberOfUpdateCalls)
            let sut = GatherInteractor(gather: GatherModel(players: [], gatherUUID: UUID()), timeHandler: GatherTimeHandler(selectedTime: mockSelectedTime))

            let mockPresenter = GatherMockPresenter()
            let exp = expectation(description: "Update gather expectation")
            mockPresenter.expectation = exp
            mockPresenter.numberOfUpdateCalls = numberOfUpdateCalls

            sut.presenter = mockPresenter

            // when
            sut.toggleTimer()
            waitForExpectations(timeout: 5) { _ in
                XCTAssertTrue(mockPresenter.timerDecrementedCalled)
                XCTAssertEqual(mockPresenter.actualUpdateCalls, numberOfUpdateCalls)
                XCTAssertEqual(sut.selectedTime.minutes, mockSelectedTime.minutes)
                XCTAssertNotEqual(sut.selectedTime.seconds, mockSelectedTime.seconds)
                sut.resetTimer()

                // then
                XCTAssertEqual(sut.selectedTime.minutes, mockSelectedTime.minutes)
                XCTAssertEqual(sut.selectedTime.seconds, mockSelectedTime.seconds)
                sut.stopTimer()
            }
        }

    }

We mock the Presenter:


    // MARK: - Presenter
    final class GatherMockPresenter: GatherPresenterServiceHandler {
        private(set) var gatherEndedCalled = false
        private(set) var serviceFailedCalled = false
        private(set) var timerDecrementedCalled = false

        weak var expectation: XCTestExpectation? = nil

        var numberOfUpdateCalls = 1
        private(set) var actualUpdateCalls = 0

        func gatherEnded() {
            gatherEndedCalled = true
            expectation?.fulfill()
        }

        func serviceFailedToEndGather() {
            serviceFailedCalled = true
            expectation?.fulfill()
        }

        func timerDecremented() {
            timerDecrementedCalled = true

            actualUpdateCalls += 1

            if expectation != nil && numberOfUpdateCalls == actualUpdateCalls {
                expectation?.fulfill()
            }
        }

    }

Key Metrics

Lines of code - Protocols

File Number of lines of code
GatherProtocols 141
PlayerListProtocols 127
ConfirmPlayersProtocols 92
PlayerEditProtocols 87
PlayerDetailProtocols 86
LoginProtocols 74
PlayerAddProtocols 73
TOTAL 680

Lines of code - View Controllers and Views

File Number of lines of code MVP-C - Lines of code MVP - Lines of code MVVM - Lines of code MVC - Lines of code
PlayerAddViewController and PlayerAddView (MVP-C & MVP) 68 129 (-61) 134 (-66) 77 (-9) 79 (-11)
PlayerListViewController and PlayerListView (MVP-C & MVP) 192 324 (-132) 353 (-161) 296 (-104) 387 (-195)
PlayerDetailViewController and PlayerDetailView (MVP-C & MVP) 74 148 (-74) 162 (-88) 96 (-22) 204 (-130)
LoginViewController and LoginView (MVP-C & MVP) 60 134 (-74) 131 (-71) 96 (-36) 126 (-66)
PlayerEditViewController and PlayerEditView (MVP-C & MVP) 106 190 (-84) 195 (-89) 124 (-18) 212 (-106)
GatherViewController and GatherView (MVP-C & MVP) 186 265 (-79) 271 (-85) 227 (-41) 359 (-173)
ConfirmPlayersViewController and ConfirmPlayersView (MVP-C & MVP) 104 149 (-45) 154 (-50) 104 260 (-156)
TOTAL 790 1339 (-549) 1400 (-610) 1020 (-230) 1627 (-837)

Lines of code - Modules

File Number of lines of code
AppModule 98
PlayerListModule 42
LoginModule 42
PlayerEditModule 41
PlayerDetailModule 41
PlayerAddModule 41
GatherModule 41
ConfirmPlayersModule 41
TOTAL 387

Lines of code - Routers

File Number of lines of code
PlayerListRouter 48
LoginRouter 32
PlayerDetailRouter 31
GatherRouter 31
ConfirmPlayersRouter 31
PlayerEditRouter 27
PlayerAddRouter 27
TOTAL 227

Lines of code - Presenters

File Number of lines of code MVP-C - LOC MVP - LOC MVVM - View Model LOC
LoginPresenter 113 111 (+2) 111 (+2) 75 (+38)
PlayerListPresenter 261 252 (+9) 259 (+2) 258 (+3)
PlayerEditPresenter 153 187 (-34) 187 (-34) 155 (-2)
PlayerAddPresenter 75 52 (+25) 52 (+25) 37 (+38)
PlayerDetailPresenter 142 195 (-53) 195 (-53) 178 (-36)
GatherPresenter 234 237 (-3) 237 (-3) 204 (+30)
ConfirmPlayersPresenter 131 195 (-64) 195 (-64) 206 (-75)
PlayerTableViewCellPresenter 55 N/A N/A N/A
PlayerDetailTableViewCellPresenter 22 N/A N/A N/A
GatherTableViewCellPresenter 22 N/A N/A N/A
ConfirmPlayersTableViewCellPresenter 22 N/A N/A N/A
TOTAL 1230 1229 (+1) 1236 (-6) 1113 (+116)

Lines of code - Interactors

File Number of lines of code
PlayerListInteractor 76
LoginInteractor 86
PlayerDetailInteractor 30
GatherInteractor 113
ConfirmPlayersInteractor 145
PlayerEditInteractor 121
PlayerAddInteractor 38
TOTAL 609

Lines of code - Local Models

File Number of lines of code MVP-C - LOC MVP - LOC
PlayerListViewState N/A 69 69
TeamSection 50 50 50
GatherTimeHandler 120 100 (+20) 100 (+20)
PlayerEditable 26 N/A N/A
PlayerDetailSection 24 N/A N/A
TOTAL 220 219 (+1) 219 (+1)

Unit Tests

Topic Data MVP-C Data MVP Data MVVM Data MVC Data
Number of key classes 53 24 +29 24 +29 14 +39 7 +46
Key Classes GatherPresenter, GatherInteractor GatherPresenter GatherPresenter GatherViewModel GatherViewController
Number of Unit Tests 17 Interactor, 29 Presenter, Total: 46 34 +12 34 +12 34 +12 30 +16
Code Coverage of Gathers feature 100% Interactor, 100% Presenter 97.2% +2.8 97.2% +2.8 97.3% +2.7 95.7% +4.3
How hard to write unit tests 1/5 3/5 -2 3/5 -2 3/5 -2 5/5 -4

Build Times

Build Time (sec)* MVP-C Time (sec)* MVP Time (sec)* MVVM Time (sec)* MVC Time (sec)*
Average Build Time (after clean Derived Data & Clean Build) 10,43 10.08 +0.35 10.18 +0.25 9.65 +0.78 9.78 +0.65
Average Build Time 0.1 0.1 0.1 0.1 0.1
Average Unit Test Execution Time (after clean Derived Data & Clean Build) 19.03 18.45 +0.58 16.52 +2.51 17.88 +1.15 12.78 +6.25

* tests were run on an 8-Core Intel Core i9, MacBook Pro, 2019.
Xcode version: 12.5.1.
macOS Big Sur 11.5.2.

Conclusion

VIPER is an awesome architecture if you are looking for clean code. We can even go further and create more layers if we truly want to embrace Single Responsibility Principle.

Unit tests are easy to write.

On the other hand, we have a lot of files, protocols and classes in our project. When something changes or we need to update something in the UI, we end up changing a lot of things and this takes time.

Specifically to our application, transforming the existing MVP-C architecture into VIPER was harder to ahieve comparing to the other patterns. We had to merge the View and ViewController layers first and then we had to touch almost all classes and creating new classes / files.

The functions are quite small, most of them concentrating on doing one thing.

The protocols files are useful if we want to use static frameworks, decoupling the modules from the main .xcodeproj.

We notice that the ViewControllers have been reduced considerably, all of them summing up almost 800 lines of code. This is more than double than the ViewControllers from MVC, where we had 1627 lines of code.

On the other hand, we now have new layers:

  • Protocols - this is just the abstractions of the modules, containing just the definition of the layers.
  • Modules - the assemble of the VIPER layers. It's part of the Router and is usually initialized via a factory.
  • Interactors - contains the business logic and network calls, orchestrating the data changes.

The new layers add 1903 lines of code.

Writing unit tests was very fun to do. We had everything decoupled and was a pleasure to assert different conditions.
We manage to obtain 100% code coverage.

However, the build times are the highest of all. Each time we delete the content of the Dervived Data folder and clean the build folder, we loose 10.43 seconds.
This takes almost one second more than the time when the app was in MVVM or MVC. But who knows how much time we saved fixing potential bugs?!

Executing unit tests after cleaning folders, takes close to 20 seconds. We have more tests, 46 in total.

More files, classes and dependencies add more time to the compiler's build time.

Lucky for us, we don't have to clean build and wipe out derived data folder each time we want to run the unit tests. We can leave this responsibility to the CI server.

I personally like using VIPER in medium to large applications that don't tend to change very often and we add new features on top of the existing ones.

However, there are some notable disadvantages when adopting VIPER.
Firstly, you write a lot of code and you might think why you need to go through three layers, instead of just doing it in the View Controller.

Secondly, it doesn't make sense for small applications, we don't need to add boilerplate to simple tasks, creating redundant files.

Finally, your app compilation time and even your startup time will increase.

Thanks for staying until the end!

Useful Links

Item Series Links
The iOS App - Football Gather GitHub Repo Link
The web server application made in Vapor GitHub Repo Link
Vapor 3 - Backend APIs article link
Migrating to Vapor 4 article link
Model View Controller (MVC) GitHub Repo Link
Article Link
Model View ViewModel (MVVM) GitHub Repo Link
Article Link
Model View Presenter (MVP) GitHub Repo Link
Article Link
Coordinator Pattern - MVP with Coordinators (MVP-C) GitHub Repo Link
Article Link
View Interactor Presenter Entity Router (VIPER) GitHub Repo Link
Article Link
View Interactor Presenter (VIP) GitHub Repo Link
Article Link