Architecture Series - Model View Presenter with Coordinators (MVP-C)
Motivation
Before diving into iOS app development, it's crucial to carefully consider the project's architecture. We need to thoughtfully plan how different pieces of code will fit together, ensuring they remain comprehensible not just today, but months or years later when we need to revisit and modify the codebase. Moreover, a well-structured project helps establish a shared technical vocabulary among team members, making collaboration more efficient.
This article kicks off an exciting series where we'll explore different architectural approaches by building the same application using various patterns. Throughout the series, we'll analyze practical aspects like build times and implementation complexity, weigh the pros and cons of each pattern, and most importantly, examine real, production-ready code implementations. This hands-on approach will help you make informed decisions about which architecture best suits your project needs.
Architecture Series Articles
- Model View Controller (MVC)
- Model View ViewModel (MVVM)
- Model View Presenter (MVP)
- Model View Presenter with Coordinators (MVP-C) - Current Article
- View Interactor Presenter Entity Router (VIPER)
- View Interactor Presenter (VIP)
If you're eager to explore the implementation details directly, you can find the complete source code in our open-source repository here.
Why Your iOS App Needs a Solid Architecture Pattern
The cornerstone of any successful iOS application is maintainability. A well-architected app clearly defines boundaries - you know exactly where view logic belongs, what responsibilities each view controller has, and which components handle business logic. This clarity isn't just for you; it's essential for your entire development team to understand and maintain these boundaries consistently.
Here are the key benefits of implementing a robust architecture pattern:
- Maintainability: Makes code easier to update and modify over time
- Testability: Facilitates comprehensive testing of business logic through clear separation of concerns
- Team Collaboration: Creates a shared technical vocabulary and understanding among team members
- Clean Separation: Ensures each component has clear, single responsibilities
- Bug Reduction: Minimizes errors through better organization and clearer interfaces between components
Project Requirements Overview
Given a medium-sized iOS application consisting of 6-7 screens, we'll demonstrate how to implement it using the most popular architectural patterns in the iOS ecosystem: MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), VIPER (View-Interactor-Presenter-Entity-Router), VIP (Clean Swift), and the Coordinator pattern. Each implementation will showcase the pattern's strengths and potential challenges.
Our demo application, Football Gather, is designed to help friends organize and track their casual football matches. It's complex enough to demonstrate real-world architectural challenges while remaining simple enough to clearly illustrate different patterns.
Core Features and Functionality
- Player Management: Add and maintain a roster of players in the application
- Team Assignment: Flexibly organize players into different teams for each match
- Player Customization: Edit player details and preferences
- Match Management: Set and control countdown timers for match duration
Screen Mockups

Backend
The application is supported by a web backend developed using the Vapor framework, a popular choice for building modern REST APIs in Swift. You can explore the details of this implementation in our article here, which covers the fundamentals of building REST APIs with Vapor and Fluent in Swift. Additionally, we have documented the transition from Vapor 3 to Vapor 4, highlighting the enhancements and new features in our article here.
What Are Coordinators?
The concept of Coordinators was first introduced by Soroush Khanlou in 2015 as a solution for managing flow logic within view controllers.
As your application grows in size and complexity, you may need to reuse view controllers in various contexts. However, coupling flow logic with the view controller makes this difficult to achieve. To implement the pattern effectively, you will need a high-level coordinator that manages the application's flow, as suggested by Soroush.
Here are a few benefits of extracting flow logic into a coordinator:
- View controllers can focus solely on their primary responsibilities, based on the architecture pattern you're using in your app (e.g., binding a model to a view).
- The initialization of view controllers is moved to a separate layer, reducing clutter in individual view controllers.
Coordinators help solve several common problems:
- Overstuffed app delegates: App delegates tend to become overloaded with responsibilities. By using a base app coordinator, we can move some of that logic to a more appropriate layer.
- Excessive responsibilities for view controllers: In architectures like MVC, view controllers often handle a variety of tasks—such as model binding, view management, data fetching, and transformation. Coordinators help alleviate this burden.
- Smoother flow: Navigation logic is extracted from view controllers and placed into coordinators, creating a more streamlined process.
The app coordinator is typically responsible for resolving the issue of an overloaded AppDelegate
.
Here, you can allocate the window object, create your navigation controller, and initialize the first view controller.
In Martin
Fowler's "Patterns of Enterprise Application Architecture", this is referred to as the Application
Controller.
A key rule for coordinators is that each coordinator maintains an array of its child coordinators. This prevents
child coordinators from being deallocated prematurely.
In the case of a tab bar application, each navigation controller has its own coordinator, which is managed by its
parent coordinator.
In addition to managing flow logic, coordinators also take over the responsibility of handling model mutations from view controllers.
Advantages
- Each view controller becomes more isolated and focused on its specific task.
- View controllers become more reusable across different parts of the app.
- Every task and sub-task in the app is encapsulated in a dedicated coordinator.
- Coordinators separate the logic of display-binding from side effects.
- Coordinators are fully under your control, making it easier to manage and extend your app's navigation flow.
The Back Navigation Problem
What happens when the user navigates back in the stack? While we can control custom back buttons, what about when the user swipes right to go back?
One way to solve this problem is to maintain a reference to the coordinator within the view controller and call its
didFinish
method inside viewDidDisappear
. This solution works for simple apps, but it
becomes problematic when multiple view controllers are managed by child coordinators.
As Soroush mentions, we can
implement the UINavigationControllerDelegate
protocol to gain control over these navigation events.
- Implement
UINavigationControllerDelegate
in your main app coordinator:
Focus on thenavigationController:didShowViewController:animated:
method, which is called after the navigation controller displays a view controller. When this event is triggered (indicating that a view controller has been popped from the stack), you can deallocate the relevant coordinators. - Subclass
UIViewController
to manage coordinators:
In this special subclass, you can maintain a dictionary of coordinators for your view controllers:
private var viewControllersToChildCoordinators: [UIViewController: Coordinator] = [:]
Implement theUINavigationControllerDelegate
within this class. When a view controller is popped and exists in the dictionary, it will be removed and deallocated.
The main tradeoff with this approach is that your subclassedUIViewController
ends up taking on more responsibilities than desired.
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 {
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?
private var loginCoordinator: LoginCoordinator? { coordinator as? LoginCoordinator }
…
}
extension LoginViewController: LoginViewDelegate {
…
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.
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(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
andPlayerListTogglable
. - 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()
func viewPlayerDetails(_ player: PlayerResponseModel)
func addPlayer()
func confirmPlayers(with playersDictionary: [TeamSection: [PlayerResponseModel]])
}
@IBAction private func confirmOrAddPlayers(_ sender: Any) {
if presenter.isInListViewMode {
delegate?.addPlayer()
} else {
delegate?.confirmPlayers(with: presenter.playersDictionary)
}
}
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
navController.pushViewController(viewController, animated: true)
}
func navigateToPlayerDetails(player: PlayerResponseModel) {
let playerDetailCoordinator = PlayerDetailCoordinator(navController: navController, parent: self, player: player)
playerDetailCoordinator.delegate = self
playerDetailCoordinator.start()
childCoordinators.append(playerDetailCoordinator)
}
func navigateToPlayerAddScreen() {
let playerAddCoordinator = PlayerAddCoordinator(navController: navController, parent: self)
playerAddCoordinator.delegate = self
playerAddCoordinator.start()
childCoordinators.append(playerAddCoordinator)
}
func navigateToConfirmPlayersScreen(with playersDictionary: [TeamSection: [PlayerResponseModel]]) {
let confirmPlayersCoordinator = ConfirmPlayersCoordinator(navController: navController, parent: self, playersDictionary: playersDictionary)
confirmPlayersCoordinator.delegate = self
confirmPlayersCoordinator.start()
childCoordinators.append(confirmPlayersCoordinator)
}
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:
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:
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)
}
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:
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:
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
And just like that, we’ve completed another Architecture Series implementation article. Congratulations!
Together, we explored how to integrate the Coordinator pattern into an existing application, simplifying the structure of our View Controllers.
Our journey began with removing all segues from the storyboards, leaving some screens seemingly isolated. If we
opened the Main.storyboard
again, we wouldn’t easily identify how the screens are connected. While it’s
possible to infer the connections based on the positioning of view controllers, this approach isn’t always intuitive
or clear.
Next, we introduced new classes at the Application level to define the main coordinator, providing a more structured flow.
Then, we took each module individually, applying the new pattern. This allowed us to streamline the passing of data between screens and manage the navigation flow more effectively. No longer did we need to perform segues or rely on the Presenter for maintaining references to the Model and View Controllers. We also removed the need for back-and-forth referencing between the Presenter and View Controller when preparing for a segue to the next screen.
Finally, we applied the Delegation pattern for communication between child and parent coordinators. For instance, when adding or editing players, the changes communicate back to the player list, triggering a refresh of the screen.
The Coordinator pattern is incredibly effective, and I believe it can be implemented in any app aiming to move away from segues and storyboards, leading to cleaner, more maintainable code.
Looking at the numbers, we introduced 348 new lines of code, but we also reduced 64 lines in the view controllers, improving readability and simplifying their logic.
Interestingly, the LoginViewController
saw an increase of three lines of code. But why is that?
In the case of the LoginViewController
, the view controller was quite simple, with just a few one-liners
for segues. After adopting the Coordinator pattern, we added two new variables:
weak var coordinator: Coordinator?
private var listCoordinator: PlayerListCoordinator? { coordinator as? PlayerListCoordinator }
The Views and Presenters generally maintained the same number of lines of code. However, there was a
small increase of 3 lines in the PlayerDetail
module, where we added three new variables to be
passed to the Edit screen (see didSelectRowAt
method). On the plus side, we reduced the 7
lines of code in the PlayerListPresenter
.
As anticipated, the primary beneficiaries of this pattern are the View Controllers.
In terms of build times, there was a slight increase due to the addition of new files, which the compiler now needs to process. With a clean build and the Derived Data folder wiped, we noticed a delay of about 2 seconds compared to the MVP version without Coordinators, and a delay of over 5 seconds compared to the MVC version.
However, this isn’t a major issue, as we typically rely on a Continuous Integration (CI) solution, and we don’t need to wait for builds locally before seeing all tests pass.
And with that, we wrap up this chapter of the series!
Useful Links
Item Series | Links |
---|---|
The iOS App - Football Gather | GitHub Repo Link |
The web server application made in Vapor |
GitHub Repo Link 'Building Modern REST APIs with Vapor and Fluent in Swift' article link 'From Vapor 3 to 4: Elevate your server-side app' 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 |