Radu Dan
MVP-C Banner

Architecture Series - Model View Presenter with Coordinators (MVP-C)

Motivation

Before starting to develop an iOS app, we have to think of 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.

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

We are going to explore how we can simplify navigation, by introducing the Coordinator pattern to our code.
As usual, we will see how we apply the pattern to each screen, seeing the actual implementation and the source code. At the end, we will show the build times and detail some key observations about MVP with Coordinators, compared to the other architecture patterns.

If you just want to see the code, feel free to skip this post. The code is available open source here.

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).

What are Coordinators

The concept of Coordinators was first brought by Soroush Khanlou, in 2015, as a solution for handling the flow logic in view controllers.

As your app grows in size and complexity, you might need to reuse some of the view controllers in new places, and by coupling the flow logic in the view controller is hard to achieve that. To execute the pattern well, you will need one high-level or base coordinator that directs the whole application, as Soroush states.

There are a few benefits of extract the flow into a coordinator:

  • View controllers can focus on their main goal, depending in what architecture pattern are you using in your app (binding a model to a view, for example).
  • The initialisation of view controllers is extracted in a different layer.

Solving problems with coordinators:

  • Overstuffed app delegates: we tend to add a lot of stuff into our app delegate, and with the use of a base app coordinator we can move some of our code over there.
  • Too many responsibilities: view controllers tend to do a lot of stuff, especially in an MVC architecture (model binding, view handling, data fetching, data transformation, etc).
  • Smooth flow: navigation flow now is moved out of the view controller and added in a coordinator.

You start with the app coordinator, solving the problem of doing so many things in the AppDelegate.
Here you can allocate the window object, create your navigation controller and initialise the first view controller. In Martin Fowlerā€™s - Patterns of Enterprise Application Architecture is called the Application Controller.

A rule of coordinators is that every coordinator holds an array of its child coordinators. In this way, we prevent the child coordinators from being deallocated.
If you have a tab bar app, each navigation controller has its own coordinator. Each coordinator is allocated by its parent coordinator.

Besides the flow logic, the coordinator also takes the responsibility from the view controllers of model mutation.

Advantages

  • Each view controller is now isolated.
  • View controller are reusable.
  • Every task and sub-task in your app has a dedicated way of being encapsulated.
  • Coordinators separate display-binding from side effects.
  • Coordinators are objects fully in your control.

The back problem

What happens when the navigation controller navigates back in the stack? For that special bar button item, we donā€™t have much control over it. We can write our own custom back button, but what happens when the user swipes right to go back?

One way of solving this problem is to keep a reference of the coordinator inside the view controller and call its didFinish method in viewDidDisappear. This is fine for a simple app, but we wonā€™t solve the problem when we have, for example, multiple view controllers shown in the child coordinator.

As Soroush mentions, we can implement UINavigationControllerDelegate to get access to these kind of events.

  1. Implement the UINavigationControllerDelegate in your main app coordinator.
    We are interested in the instance method navigationController:didShowViewController:animated:, which is called just after the navigation controller displays a view controllerā€™s view and navigation item properties.
    When you get a triggered event that a view controller has been popped from the view stack, you can deallocate the relevant coordinators.
  2. Subclass UIViewController and make your it part of your flow.
    In this special subclass you will have a dictionary that keeps the entries of your coordinators:
    private var viewControllersToChildCoordinators: [UIViewController: Coordinator] = [:]
    You implement the UINavigationControllerDelegate in this class. When a view controller is popped and is part of the dictionary, it will be removed and deallocated.
    The downside and main tradeoff with this approach is that your special subclass (which is a UIViewController) does more than we want.

Applying to our code

We start first with defining our application coordinators:


    protocol Coordinator: AnyObject {
        var childCoordinators: [Coordinator] { get set }
        var parent: Coordinator? { get set }

        func start()
    }

The start function takes care of allocating the view controller and pushing it into the navigation controller stack.


    protocol Coordinatable: AnyObject {
        var coordinator: Coordinator? { get set }
    }

We define a Coordinatable project that our view controllers will implement, so they can delegate to their coordinator specific navigation tasks (such as going back).

Next, we create our main app coordinator: AppCoordinator and initialise it within the AppDelegate.


    final class AppCoordinator: NSObject, Coordinator {

        weak var parent: Coordinator?
        var childCoordinators: [Coordinator] = []

        private let navController: UINavigationController
        private let window: UIWindow

        init(navController: UINavigationController = UINavigationController(),
             window: UIWindow = UIWindow(frame: UIScreen.main.bounds)) {
            self.navController = navController
            self.window = window
        }

        func start() {
            navController.delegate = self
            window.rootViewController = navController
            window.makeKeyAndVisible()
        }

    }

The AppDelegate now looks like this:


    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {

        // We want to initialise the window object after did finish launching with options
        private lazy var appCoordinator = AppCoordinator()

        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            appCoordinator.start()
            return true
        }
    ā€¦
    }
    

Our first screen is Login. We do the following adjustments so it can support coordinators:


    final class LoginViewController: UIViewController, Coordinatable {

        @IBOutlet weak var loginView: LoginView!

        weak var coordinator: Coordinator?
        // For convenience we cast the coordinator to the specific screen Coordinator.
        private var loginCoordinator: LoginCoordinator? { coordinator as? LoginCoordinator }

    ā€¦
    }

    extension LoginViewController: LoginViewDelegate {
    ā€¦
        // Segues are not performed any more, we let this job to be done by the Coordinator.
        func didLogin() {
            loginCoordinator?.navigateToPlayerList()
        }

        func didRegister() {
            loginCoordinator?.navigateToPlayerList()
        }
    }

The LoginCoordinator is presented below:


    final class LoginCoordinator: Coordinator {

        weak var parent: Coordinator?
        var childCoordinators: [Coordinator] = []

        private let navController: UINavigationController

        init(navController: UINavigationController, parent: Coordinator? = nil) {
            self.navController = navController
            self.parent = parent
        }

        func start() {
            let viewController: LoginViewController = Storyboard.defaultStoryboard.instantiateViewController()
            viewController.coordinator = self
            navController.pushViewController(viewController, animated: true)
        }

        func navigateToPlayerList() {
            let playerListCoordinator = PlayerListCoordinator(navController: navController, parent: self)
            playerListCoordinator.start()
            childCoordinators.append(playerListCoordinator)
        }

    }

An alternative to Coordinatable, is to use delegation i.e. create a LoginViewControllerDelegate that contains the method navigateToPlayerList, and make LoginCoordinator the delegate of this class.

And the final piece for LoginScreen, is to remove the segues from the storyboard.

Storyboard

As we are going to instantiate all of our view controllers from the storyboard, letā€™s define a convenient method to do that:


    enum Storyboard: String {
        case main = "Main"

        static var defaultStoryboard: UIStoryboard {
            return UIStoryboard(name: Storyboard.main.rawValue, bundle: nil)
        }
    }

    extension UIStoryboard {
        func instantiateViewController<T>(withIdentifier identifier: String = String(describing: T.self)) -> T {
            return instantiateViewController(withIdentifier: identifier) as! T
        }
    }

We now can allocate a view controller by setting the storyboard ID in the designated storyboard and use:

let viewController: PlayerListViewController = Storyboard.defaultStoryboard.instantiateViewController()

PlayerList screen suffers the following adjustments:

  • We removed PlayerListTogglable, the pop functionality resides completely in the responsibilities of Coordinators.
  • Make it implement Coordinatable so we can have a reference to the View Controllerā€™s coordinator.
  • Remove the delegate methods from PlayerDetailViewControllerDelegate, AddPlayerDelegate and PlayerListTogglable.
  • Enhance the public API with the methods that will be required after a player is edited (reload data), added and after a gather finishes (toggleViewState).

    weak var coordinator: Coordinator?
    private var listCoordinator: PlayerListCoordinator? { coordinator as? PlayerListCoordinator }

    /// .....

    func reloadView() {
        playerListView.loadPlayers()
    }
    func didEdit(player: PlayerResponseModel) {
        playerListView.didEdit(player: player)
    }

    func toggleViewState() {
        playerListView.toggleViewState()
    }

To navigate to different screens from PlayerList (to Add or Edit screens, for example), we created the appropriate segue identifier in the Presenter and forward it to the ViewController using the View layer. We now deprecated the use of segue identifiers, all routing will be done using Coordinators. So, letā€™s implement these changes:


    protocol PlayerListViewDelegate: AnyObject {
        func didRequestToChangeTitle(_ title: String)
        func addRightBarButtonItem(_ barButtonItem: UIBarButtonItem)
        func presentAlert(title: String, message: String)
        func didRequestPlayerDeletion()
        // New methods defined below
        func viewPlayerDetails(_ player: PlayerResponseModel)
        func addPlayer()
        func confirmPlayers(with playersDictionary: [TeamSection: [PlayerResponseModel]])
    }

    // Removed func confirmOrAddPlayers(withSegueIdentifier segueIdentifier: String) and func didRequestPlayerDetails()

    @IBAction private func confirmOrAddPlayers(_ sender: Any) {
        // Checks what action we should perform
        if presenter.isInListViewMode {
            delegate?.addPlayer()
        } else {
            delegate?.confirmPlayers(with: presenter.playersDictionary)
        }
    }

    // In didSeletRow method, we retrieve the player model object and pass it to the ViewController, which will pass it to the PlayerListCoordinator.
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard !presenter.playersCollectionIsEmpty else { return }

        if presenter.isInListViewMode {
            let player = presenter.selectPlayerForDisplayingDetails(at: indexPath)
            delegate?.viewPlayerDetails(player)
        } else {
            toggleCellSelection(at: indexPath)
            updateViewForPlayerSelection()
        }
    }

PlayerListCoordinator

The coordinator implementation is presented below:


    final class PlayerListCoordinator: Coordinator {

        weak var parent: Coordinator?
        var childCoordinators: [Coordinator] = []

        private let navController: UINavigationController
        private var playerListViewController: PlayerListViewController?

        init(navController: UINavigationController, parent: Coordinator? = nil) {
            self.navController = navController
            self.parent = parent
        }

        func start() {
            let viewController: PlayerListViewController = Storyboard.defaultStoryboard.instantiateViewController()
            viewController.coordinator = self
            playerListViewController = viewController
            navController.pushViewController(viewController, animated: true)
        }

        // Passes the player created for the row where the user tapped
        func navigateToPlayerDetails(player: PlayerResponseModel) {
            let playerDetailCoordinator = PlayerDetailCoordinator(navController: navController, parent: self, player: player)
            playerDetailCoordinator.delegate = self
            playerDetailCoordinator.start()
            childCoordinators.append(playerDetailCoordinator)
        }

        // Go to PlayerDetails screen
        func navigateToPlayerAddScreen() {
            let playerAddCoordinator = PlayerAddCoordinator(navController: navController, parent: self)
            playerAddCoordinator.delegate = self
            playerAddCoordinator.start()
            childCoordinators.append(playerAddCoordinator)
        }

        // Next screen in the app flow
        func navigateToConfirmPlayersScreen(with playersDictionary: [TeamSection: [PlayerResponseModel]]) {
            let confirmPlayersCoordinator = ConfirmPlayersCoordinator(navController: navController, parent: self, playersDictionary: playersDictionary)
            confirmPlayersCoordinator.delegate = self
            confirmPlayersCoordinator.start()
            childCoordinators.append(confirmPlayersCoordinator)
        }

    }

    // We use delegation to listen for child coordinators flow actions
    extension PlayerListCoordinator: PlayerAddCoordinatorDelegate {
        func playerWasAdded() {
            playerListViewController?.reloadView()
        }
    }

    extension PlayerListCoordinator: PlayerDetailCoordinatorDelegate {
        func didEdit(player: PlayerResponseModel) {
            playerListViewController?.didEdit(player: player)
        }
    }

    extension PlayerListCoordinator: ConfirmPlayersCoordinatorDelegate {
        func didEndGather() {
            playerListViewController?.toggleViewState()

            if let playerListViewController = playerListViewController {
                navController.popToViewController(playerListViewController, animated: true)
            }
        }
    }
    

There are quite a few changes we had to do to PlayerEdit and PlayerDetail screens.

Firstly, we had to make them implement Coordinatable, so we can have a reference to the coordinators, same as we did for PlayerList.

In PlayerDetails, we had to make setupTitle method public, because when we edit a player and change its name, we will need to communicate this change to the ViewController so it can refresh the navigation title. The title is actually the player name.

Same thing we did to reloadData(), and created a new function updateData(player) to communicate to the View the player changes.


    func reloadData() {
       playerDetailView.reloadData()
    }

    func updateData(player: PlayerResponseModel) {
       playerDetailView.updateData(player: player)
    }

We use PlayerDetailViewDelegate to listen to changes that happen in the View layer:


    extension PlayerDetailViewController: PlayerDetailViewDelegate {
        func didRequestEditView(with viewType: PlayerEditViewType,
                                playerEditModel: PlayerEditModel?,
                                playerItemsEditModel: PlayerItemsEditModel?) {

            detailCoordinator?.navigateToEditScreen(viewType: viewType,
                                                    playerEditModel: playerEditModel,
                                                    playerItemsEditModel: playerItemsEditModel)
        }
    }

PlayerDetailViewDelegate has now changed the simple didRequestEditView method, into the one that you see above.
This is called from didSelectRow table view's delegate:


    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
       presenter.selectPlayerRow(at: indexPath)

       delegate?.didRequestEditView(with: presenter.destinationViewType,
                                    playerEditModel: presenter.playerEditModel,
                                    playerItemsEditModel: presenter.playerItemsEditModel)
    }

PlayerDetailCoordinator

Full code below:


    // Communicate up in the view controllers stacks what changes were done
    protocol PlayerDetailCoordinatorDelegate: AnyObject {
        func didEdit(player: PlayerResponseModel)
    }

    final class PlayerDetailCoordinator: Coordinator {

        weak var parent: Coordinator?
        var childCoordinators: [Coordinator] = []
        weak var delegate: PlayerDetailCoordinatorDelegate?

        private let navController: UINavigationController
        private let player: PlayerResponseModel
        private var detailViewController: PlayerDetailViewController?

        init(navController: UINavigationController, parent: Coordinator? = nil, player: PlayerResponseModel) {
            self.navController = navController
            self.parent = parent
            self.player = player
        }

        func start() {
            let viewController: PlayerDetailViewController = Storyboard.defaultStoryboard.instantiateViewController()
            viewController.coordinator = self
            viewController.player = player
            detailViewController = viewController
            navController.pushViewController(viewController, animated: true)
        }

        func navigateToEditScreen(viewType: PlayerEditViewType,
                                  playerEditModel: PlayerEditModel?,
                                  playerItemsEditModel: PlayerItemsEditModel?) {
            let editCoordinator = PlayerEditCoordinator(navController: navController,
                                                        viewType: viewType,
                                                        playerEditModel: playerEditModel,
                                                        playerItemsEditModel: playerItemsEditModel)
            editCoordinator.delegate = self
            editCoordinator.start()
            childCoordinators.append(editCoordinator)
        }
    }

    extension PlayerDetailCoordinator: PlayerEditCoordinatorDelegate {
        func didFinishEditing(player: PlayerResponseModel) {
            detailViewController?.setupTitle()
            detailViewController?.updateData(player: player)
            detailViewController?.reloadData()
            delegate?.didEdit(player: player)
        }
    }

PlayerEditCoordinator

The implementation is pretty straightforward:


    // Communicate up in the view controllers stacks what changes were done
    protocol PlayerEditCoordinatorDelegate: AnyObject {
        func didFinishEditing(player: PlayerResponseModel)
    }

    final class PlayerEditCoordinator: Coordinator {

        weak var parent: Coordinator?
        var childCoordinators: [Coordinator] = []
        weak var delegate: PlayerEditCoordinatorDelegate?

        private let navController: UINavigationController
        private let viewType: PlayerEditViewType
        private let playerEditModel: PlayerEditModel?
        private let playerItemsEditModel: PlayerItemsEditModel?

        init(navController: UINavigationController,
             parent: Coordinator? = nil,
             viewType: PlayerEditViewType,
             playerEditModel: PlayerEditModel?,
             playerItemsEditModel: PlayerItemsEditModel?) {
            self.navController = navController
            self.parent = parent
            self.viewType = viewType
            self.playerEditModel = playerEditModel
            self.playerItemsEditModel = playerItemsEditModel
        }

        func start() {
            let viewController: PlayerEditViewController = Storyboard.defaultStoryboard.instantiateViewController()
            viewController.coordinator = self
            viewController.viewType = viewType
            viewController.playerEditModel = playerEditModel
            viewController.playerItemsEditModel = playerItemsEditModel
            navController.pushViewController(viewController, animated: true)
        }

        // Called from the ViewController
        func didFinishEditingPlayer(_ player: PlayerResponseModel) {
            delegate?.didFinishEditing(player: player)
            navController.popViewController(animated: true)
        }
    }

PlayerAddCoordinator

The add players feature is impacted a little, because itā€™s very simple. The coordinator looks like this:


    protocol PlayerAddCoordinatorDelegate: AnyObject {
        func playerWasAdded()
    }

    final class PlayerAddCoordinator: Coordinator {

        weak var parent: Coordinator?
        var childCoordinators: [Coordinator] = []
        weak var delegate: PlayerAddCoordinatorDelegate?

        private let navController: UINavigationController

        init(navController: UINavigationController, parent: Coordinator? = nil) {
            self.navController = navController
            self.parent = parent
        }

        func start() {
            let viewController: PlayerAddViewController = Storyboard.defaultStoryboard.instantiateViewController()
            viewController.coordinator = self
            navController.pushViewController(viewController, animated: true)
        }

        func playerWasAdded() {
            delegate?.playerWasAdded()
            navController.popViewController(animated: true)
        }

    }

In PlayerAddViewController, we modify didAddPlayer (that is called from the View layer) as presented below:


    func didAddPlayer() {
        // The delegate is now the addCoordinator.
        // We remove navigationController?.popViewController(animated: true), because we handle this in addCoordinator.
        addCoordinator?.playerWasAdded()
    }

ConfirmPlayersCoordinator

In ConfirmPlayers, we take in the selected players dictionary, we choose a team for them and finally we start the gather.

ConfirmPlayersCoordinator looks like this:


    // Notify PlayerListCoordinator that we finished the gather and is time to toggle the view state
    protocol ConfirmPlayersCoordinatorDelegate: AnyObject {
        func didEndGather()
    }

    final class ConfirmPlayersCoordinator: Coordinator {

        weak var parent: Coordinator?
        var childCoordinators: [Coordinator] = []
        weak var delegate: ConfirmPlayersCoordinatorDelegate?

        private let navController: UINavigationController
        private let playersDictionary: [TeamSection: [PlayerResponseModel]]

        init(navController: UINavigationController, parent: Coordinator? = nil, playersDictionary: [TeamSection: [PlayerResponseModel]] = [:]) {
            self.navController = navController
            self.parent = parent
            self.playersDictionary = playersDictionary
        }

        func start() {
            let viewController: ConfirmPlayersViewController = Storyboard.defaultStoryboard.instantiateViewController()
            viewController.coordinator = self
            viewController.playersDictionary = playersDictionary
            navController.pushViewController(viewController, animated: true)
        }

        func navigateToGatherScreen(with gatherModel: GatherModel) {
            let gatherCoordinator = GatherCoordinator(navController: navController, parent: self, gather: gatherModel)
            gatherCoordinator.delegate = self
            gatherCoordinator.start()
            childCoordinators.append(gatherCoordinator)
        }
    }

    extension ConfirmPlayersCoordinator: GatherCoordinatorDelegate {
        func didEndGather() {
            delegate?.didEndGather()
        }
    }

In ConfirmPlayersView we changed the method didStartGather() and passed the GatherModel in the parameter list: func didStartGather(_ gather: GatherModel).

GatherCoordinator

Finally, GatherCoordinator is detailed below:


    // Notifies Confirmation screen that a gather has ended.
    protocol GatherCoordinatorDelegate: AnyObject {
        func didEndGather()
    }

    final class GatherCoordinator: Coordinator {

        weak var parent: Coordinator?
        var childCoordinators: [Coordinator] = []
        weak var delegate: GatherCoordinatorDelegate?

        private let navController: UINavigationController
        private let gather: GatherModel

        init(navController: UINavigationController, parent: Coordinator? = nil, gather: GatherModel) {
            self.navController = navController
            self.parent = parent
            self.gather = gather
        }

        func start() {
            let viewController: GatherViewController = Storyboard.defaultStoryboard.instantiateViewController()
            viewController.coordinator = self
            viewController.gatherModel = gather
            navController.pushViewController(viewController, animated: true)
        }

        // Called from the ViewController
        func didEndGather() {
            delegate?.didEndGather()
        }

    }

GatherViewControllerā€™s didEndGather method has reduced considerably from:


    func didEndGather() {
        guard let playerListTogglable = navigationController?.viewControllers.first(where: { $0 is PlayerListTogglable }) as? PlayerListTogglable else {
            return
        }

        playerListTogglable.toggleViewState()

        if let playerListViewController = playerListTogglable as? UIViewController {
            navigationController?.popToViewController(playerListViewController, animated: true)
        }
    }

To:


    func didEndGather() {
        gatherCoordinator?.didEndGather()
    }

Key Metrics

Lines of code - Coordinators

File Number of lines of code
PlayerListCoordinator 74
GatherCoordinator 41
PlayerEditCoordinator 51
PlayerDetailCoordinator 59
PlayerAddCoordinator 39
LoginCoordinator 35
ConfirmPlayersCoordinator 49
TOTAL 348

Lines of code - View Controllers

File Number of lines of code MVP - Lines of code MVVM - Lines of code MVC - Lines of code
PlayerAddViewController 54 59 (-5) 77 (-23) 79 (-25)
PlayerListViewController 86 115 (-29) 296 (-210) 387 (-301)
PlayerDetailViewController 68 85 (-18) 96 (-28) 204 (-136)
LoginViewController 46 43 (+3) 96 (-50) 126 (-80)
PlayerEditViewController 63 68(-5) 124 (-61) 212 (-149)
GatherViewController 67 73 (-6) 227 (-160) 359 (-292)
ConfirmPlayersViewController 46 51 (-5) 104 (-58) 260 (-214)
TOTAL 430 494 (-64) 1020 (-590) 1627 (-1197)

Lines of code - Views

File Number of lines of code MVP - Lines of code
PlayerAddView 75 75
PlayerListView 238 238
PlayerDetailView 80 77 (+3)
LoginView 88 88
PlayerEditView 127 127
GatherView 198 198
ConfirmPlayersView 103 103
TOTAL 909 906 (+3)

Lines of code - Presenters

File Number of lines of code MVP - Lines of code MVVM - View Model LOC
LoginPresenter 111 111 75 (+36)
PlayerListPresenter 252 259 (-7) 258 (-6)
PlayerEditPresenter 187 187 155 (+32)
PlayerAddPresenter 52 52 37 (+15)
PlayerDetailPresenter 195 195 178 (+17)
GatherPresenter 237 237 204 (+33)
ConfirmPlayersPresenter 195 195 206 (-11)
TOTAL 1229 1236 (-7) 1113 (+116)

Lines of code - Local Models

File Number of lines of code MVP - Lines of code
PlayerListViewState 69 69
TeamSection 50 50
GatherTimeHandler 100 100
TOTAL 219 219

Unit Tests

Topic Data MVP Data MVVM Data MVC Data
Number of key classes (ViewControllers, Views, Presenters, Local Models) 24 24 14 +10 7 +17
Key Class GatherPresenter GatherPresenter GatherViewModel GatherViewController
Number of Unit Tests 34 34 34 30 +4
Code Coverage of Gathers feature 97.2% 97.2% 97.3% -0.1 95.7% +1.5
How hard to write unit tests 3/5 3/5 3/5 5/5 -2

Build Times

Build Time (sec)* MVP Time (sec)* MVVM Time (sec)* MVC Time (sec)*
Average Build Time (after clean Derived Data & Clean Build) 10.08 10.18 -0.1 9.65 +0.43 9.78 +0.3
Average Build Time 0.1 0.1 0.1 0.1
Average Unit Test Execution Time (after clean Derived Data & Clean Build) 18.45 16.52 +1.93 17.88 +0.57 12.78 +5.67

* 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

Another one bites the dust. Congrats! We have finished another Architecture Series implementation article.

We discovered together how we can implement the Coordinator pattern to an existing application, simplifying the View Controllers.

Firstly, we had to take off all segues from the storyboards, leaving some hanging screens there. If we would open again the Main.storyboard we wouldn't know how the screens are connected. We can find that somehow from how the view controllers are positioned, but this isn't very intuitive in all cases.

Then, we introduced some new classes at the Application level to create the main coordinator.

Next, we took module by module and applied the new pattern, simplifying how things are passed between the screens and how we initiate the next step in our journey. We no longer need to perform segues, hold a reference in the Presenter of the Model and when the View Controller is prepared to perform the segue to go way back to the Presenter (or hold a reference in the ViewController) to retrieve the Model we need for the next screen.

Finally, we implemented the Delegation pattern to communicate from child to parent coordinators (e.g. adding or editing players communicating back to the player list to refresh the screen).

I think this is a great pattern and can be used in all apps that want to move away from segues and Storyboards.

By looking at the number of lines of code, we have introduced 348 new lines.

However, we now have 64 LOC less in the view controllers.

As we can see in LoginViewController we increased the LOC with three. Quite unusual...why is that?!

Well, view controller is simple and it had just some one liners when performing the segues. When adopting the Coordinator pattern we introduced two new variables:


    weak var coordinator: Coordinator?
    private var listCoordinator: PlayerListCoordinator? { coordinator as? PlayerListCoordinator }

The Views and the Presenters have kept mostly the same number of LOC.
Some small difference in the PlayerDetail module, where we introduced 3 new LOC in PlayerDetailView, because we introduced three new variables to be passed to the Edit screen (see didSelectRowAt method). However, we managed to remove 7 LOC from PlayerListPresenter.

So, as expected, the main beneficiaries of this pattern are the View Controllers.

The build times have increased a little, probably because we have introdued new files and the compiler needs to do more stuff. Each time we are doing a clean build and wiping the Derived Data folder, we loose almost 2 seconds compared to the app coded in MVP without Coordinators and more than 5 seconds when the app was using MVC.

This is not catastrophic, we usually use a CI solution for this and we don't need to wait locally to have all tests green.

Aaand that's a wrap!

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