Lightweight Design Patterns in iOS (Part 3) - Coordinator

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. So complex sometimes, 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 on 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 and 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 push 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)
    }
}

Marvellous! ๐Ÿ’„

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, follow me on Twitter, Iโ€™ll be happy to answer any of your questions and youโ€™ll be the first one to know when a new article comes out! ๐Ÿ‘Œ

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

Bye bye! ๐Ÿ‘‹