Lightweight Design Patterns in iOS (Part 3) - Coordinator

Jean Mainguy
EGYM Software Development
4 min readNov 19, 2018

--

Pssst! I started my own blog!
You can read this very same article over there too!
No paywall, no ad, no Javascript — no bullshit, just pure content.
See you there!

Design Patterns are part of Mobile Development for a while now and a revolution towards Quality Assurance is on. Such patterns have become a standard among the community, yet implementing an academic one might come with a price — complexity.

Because some Design Patterns solve very complex problems, they often result in a complicated implementation as well. Sometimes so complex, that we tend to use heavy Third-Party Frameworks, even for simple use cases.

The time has come to combine quality and simplicity.

🎬 Hi there, I’m Jean!

Previously in the series Lightweight Design Patterns in iOS, we have covered the case of the Presenter Pattern or how to implement a lightweight presentation layer! 📺
In this episode, we will take a quick look at the Coordinator Pattern, the problem it solves as well as how to implement it in a simple yet powerful manner! 🚦

Coordinator Pattern

The role of the Coordinator is to coordinate the navigation between screens of the same flow. 🗺
This means in Mobile Development: defining a user flow! 📲
In iOS, the Storyboard is already providing this feature but it also comes with considerable drawbacks, such as loading time and testability. 😕
The Coordinator allows you to separate the Navigation logic from the ViewController and define your screen flow directly in code! 💯

Let’s now look at an example! 👀

The Coordinator in practice

We have an app, that displays a list of “things” and tapping on one of them opens a detail screen with more information concerning that item. Easy. 👍

Our navigation is going to look very concise: we will first start with a list of things and later on eventually open something. This flow definition is nothing else than our CoordinatorProtocol! 👌

protocol CoordinatorProtocol: class {
func start()
func open(_ something: Something)
}

Looking good! 😎
Let’s now implement the actual Coordinator!

final class Coordinator {
private let window: UIWindow
let navigationController = UINavigationController()

init(window: UIWindow) {
self.window = window
self.window.rootViewController = navigationController
self.window.makeKeyAndVisible()
}
}

As you can see, we need to inject a UIWindow in the init method, in order to make the navigationController the rootViewController of this window. 📱

Now, implementing the CoordinatorProtocol will only consist of instantiating the ViewControllers with their dependencies injected and pushing them to the NavigationController! 💯

extension Coordinator: CoordinatorProtocol {
func start() {
let viewController = MainViewController()
viewController.viewModel = MainViewModel()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
}

func open(_ something: Something) {
let viewController = SomethingViewController()
viewController.viewModel = SomethingViewModel()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
}
}

And that’s it! Our flow is now implemented, and can be started/continued from anywhere in the App! 🎉

Let’s start with the AppDelegate!

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var coordinator: CoordinatorProtocol!

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
coordinator = Coordinator()

return true
}

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
coordinator.start()

return true
}
}

Nice and simple! Now let’s look at the MainViewController!

final class MainViewController: UITableViewController {
weak var coordinator: CoordinatorProtocol?
var viewModel: SomethingViewModel!

...

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let something = viewModel.somethings[indexPath.row]
coordinator?.open(something)
}

...
}

That’s it! The entire Navigation Logic has just been delegated to an independent layer! 🎊

Testing the Navigation Logic

Unit testing the navigation has never been easier!
Before writing any test, let’s implement a CoordinatorMock first! 🐚

final class CoordinatorMock: CoordinatorProtocol {
private(set) var isStarted = false
private(set) var isSomethingOpened = false

func start() {
isStarted = true
}

func open(_ something: Something) {
isSomethingOpened = true
}
}

As you can see, we are just implementing the CoordinatorProtocol and toggle the respective boolean properties whenever a method is called. 🚀
Simple right?! Well, look at how easy testing now is! 💪

Let’s start with the AppDelegate and verify the start method is called!

final class AppDelegateTest: XCTestCase {
var sut: AppDelegate!

override func setUp() {
super.setUp()
sut = AppDelegate()
}

func testAppDelegateStartsCoordinatorSuccessfully() {
let coordinator = CoordinatorMock()
sut.coordinator = coordinator

_ = sut.application(UIApplication.shared, didFinishLaunchingWithOptions: [:])

XCTAssertTrue(coordinator.isStarted)
}
}

Marvelous! 💄

And now with the MainViewController, let’s make sure the open function is called!

final class MainViewControllerTest: XCTestCase {
var sut: MainViewController!

override func setUp() {
super.setUp()
sut = MainViewController()
sut.viewModel = SomethingViewModel()

_ = sut.view
}

func testMainViewControllerOpensSomethingSuccessfully() {
let coordinator = CoordinatorMock()
sut.coordinator = coordinator

sut.tableView(sut.tableView, didSelectRowAt: IndexPath(item: 0, section: 0))

XCTAssertTrue(coordinator.isSomethingOpened)
}
}

And boom! 💥
We just made a lightweight Coordinator which is even testable! 🎉

Worth it? Did that help you see what problems the Coordinator Pattern is trying to solve? How to make such a pattern lightweight?

If so:

  1. Let me know in the comments below! 👇
  2. Don’t forget to hit that subscribe button! ✅
  3. Follow me on Twitter, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌

See you next week, for Part 4 of my series Lightweight Design Patterns in iOS!

Bye-bye! 👋

--

--