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! πŸ‘‹