Skip to content

Navigation & Coordinators

lmarceau edited this page Jan 10, 2025 · 5 revisions

Context

Coordinators are the current pattern used in our application to navigate. Coordinators are a specialized type of delegate that lets us remove the job of app navigation from our view controllers. They’ve been around for a while (since 2015) and are used in a myriad of applications. They help to make our view controllers more manageable and reusable, while also letting us adjust our app's flow whenever we need. View controllers work best when they are stand alone in an app, unaware of their position in the app’s flow, or even that they are part of a flow in the first place. Not only does this help make our code easier to test and reason about, but it also allows you to re-use view controllers elsewhere in our app more easily. In other words, ViewController A is not aware it's presenting ViewController B, and ViewController B doesn't know it can also be presented from ViewController C.

Original motivation to implement the Coordinators pattern

One of the challenges of working in Firefox for iOS is making changes inside our massive class called BrowserViewController. This class handles and holds pretty much everything of what makes Firefox iOS the application it is. It was also deciding what is shown and when, on top of holding a reference to all the views. Navigation code was sometimes duplicated, since each navigation path was coupled with the previous navigation or view controller. Unit tests were non-existent for any navigation path. The consequence of that is anytime we need to make changes to navigation code, repercussions often happens in production. Some navigation path are intricate to test and requires deep knowledge of the system. This increased our development time and decrease the maintainability of our application. BrowserViewController was also hiding and showing the homepage in a way that isn’t a standard iOS pattern. This introduced a number of issues when adding features on the homepage, since standard lifecycle methods weren’t called on it. As a first step of reducing the scope of the BrowserViewController and fixing the appearance of the homepage, coordinators were chosen as a pattern to fix those problems.

Pattern choice

There are different ways of implementing the coordinator pattern inside an application. Common solutions to pitfalls were investigated in the document here. The recommended solution after investigation is the Router pattern, which is a class that wraps a UINavigationController to pass it around between coordinators.

What should a Router do

The Router is a class that wraps a UINavigationController to pass it around between coordinators. We extend a Router class to conform to UINavigationControllerDelegate, so the router can handle all the events for the navigation controller. It wraps and delegate the responsibility for what to do on the back button event back to the coordinator that pushed this coordinator in the first place.

What should a Coordinator do

To ensure we don’t end up with massive coordinators and properly remove tight coupling between view controllers, we should follow some principles.

  • Coordinator should control the flow in our app, which means creating VCs and view models and calling the router to present and push. It shouldn’t decide what to show or contain business logic. The one exception for this is that coordinator can contain A/B tests variants since it’s convenient.
  • Coordinator should manage only view controllers.
  • We do have exceptions for this in our code, but coordinators shouldn't hold references to the shown view controllers. Unless there's specific reasons to do so, please avoid keeping a reference to the view controller.

Architecture implemented

This section explains the architecture implemented for v114. There will be subsequent work coming to introduce the Coordinator pattern even more in our application.

Overview

The following coordinators are currently implemented. Each Scene holds its own SceneCoordinator.

Screenshot 2023-04-25 at 4 19 01 PM

Launch

At launch of the application, we now have a second LaunchScreen that helps us determine into which path we should fall into. This launch screen appears for a split second, and is there basically to asynchronously determine if the SceneCoordinator should present different onboarding or navigate to the browser directly. The following image illustrate what screens appears to the user when we launch the application on first app install (it shows the onboarding).

Screenshot 2023-04-25 at 4 15 18 PM

BrowserCoordinator

homepageViewController refers to the legacyHomepageViewController here.

The BrowserCoordinator is currently our central piece. More will be handled in other coordinators in the future, but for now this coordinator holds the browserViewController and homepageViewController, as well as managing almost all the deep links (more on that in the next section). The BrowserCoordinator is responsible for embedding the home page and web view inside a container, successfully removing this responsibility that was previously hold by the browserViewController. BrowserViewController isn't directly aware of what the container holds.

Note that browserViewController and homepageViewController are kept as strong reference under the BrowserCoordinator because:

  • The BrowserCoordinator needs to embed the webview inside the BrowserViewController. The WebViewViewController cannot be removed entirely from the view hierarchy since the web view cannot reload properly the webpage otherwise.
  • The HomepageViewController is kept as a reference since its data is not part of redux. Each time the HomepageViewController is created, the data is fetched again from network. To ensure the loading of the homepage is not expensive, its kept as a reference under the browser coordinator.

Deep links

The SceneDelegate receives connecting options on its delegate method from different user actions. It can either be of URL format, user activity format or shortcut format. Those options gets parsed as one of our Route enum case, which is then passed down from SceneCoordinator to child coordinators to be handled so we navigate to the proper screen being asked by the user action.

Clone this wiki locally