Implement Coordinator Design Pattern Using Combine | by Zafar Ivaev | May, 2022

Navigate and pass data between screens reactively

Photo by Linus Mimietz on Unsplash

This article will learn how to create a reusable Coordinator library, which will be responsible for navigation inside your iOS apps.

In short, this is what you will have learned by the end of the tutorial:

  • What is a Coordinator design pattern
  • How to use Combine to make Coordinator reactive.

The finished package and its example application are available at the bottom of the article.

Before we start developing the framework, you should know that Coordinator design pattern helps us move navigation-related logic from the view controller. As a result, the view controller has a strong dependency on the coordinator, while the coordinator has a weak dependency on the view controller. View controller fires necessary navigation methods when an action happens, for example, if the user taps on a button.

Different teams prefer different relationships between a view controller and a coordinator. Some put the Coordinator property inside a view model or a presenter, while others do so inside the view controller. In this tutorial, we will follow the latter approach, since the example application is not going to be complex enough to include other entities like view models or presenters.

Without further ado, let’s dive into the code.

First, we create a Coordinator.swift file and add a generic Coordinator class:

The CoordinationResult is a placeholder for the type which is going to be passed between two particular coordinators. When no value needs to be passed, we will use Void as the type. Later in this tutorial, we will see how to pass custom objects between screens.

Now, let’s add two methods that will be the core of starting the work of a coordinator and navigating to other coordinators:

Here we add the coordinate<T>(to coordinator: Coordinator<T>) method, which in its turn will simply call the start() method of the coordinator we want to navigate to.

The start() method contains a fatalError()since we design this Coordinator class to be subclassed and not used as-is. In other words, every single concrete Coordinator will have to provide its own implementation of the start() method.

In both methods, we return AnyPublisher<T, Never>, which represents the output of a coordinator that is being navigated to. Later we will see how to use this to make passing data possible.

With the framework done, let’s now use what we have created in an example application.

Our goal is to cover these use cases:

  • Show a welcome page, then on button tap present the main flow with the tab bar.
  • Present a view controller inside the main flow and then pass the data back to the presenting view controller.

First of all, we need to create an AppCoordinatorwhich will be responsible for setting rootViewController for the UIWindow. Here’s the code to do that:

Here is what we achieve here:

  1. Add an unowned dependency on the application’s UIWindow.
  2. Provide an initializer.
  3. Implement the start() method by creating a UINavigationController and setting it as the rootViewController of the window. Then, coordinate to the WelcomeCoordinatorwhich we are going to implement next.

Now, let’s use the AppCoordinator inside the SceneDelegate to start the flow of our app, as shown below:

  1. We add a strong dependency on the AppCoordinator inside the SceneDelegate.
  2. In the scene(_scene:willConnectTo:...) method, initialize the AppCoordinator with the UIWindow. Finally, start the work of the coordinator.

Now that setup is done, let’s continue with the Welcome page. We have a simple WelcomeViewController which displays a button in the center of the screen. The code is below:

  1. We add a dependency on the yet-to-be implemented WelcomeCoordinator.
  2. In the viewDidLoad() we launch binding and UI setup methods.
  3. In the bindButtonToCoordinator() method, we create a publisher for button taps, on each of which we fire the coordinator’s navigateToMain() method.
  4. We create and layout a UIButton in a standard way.

Let’s implement the WelcomeCoordinator now:

  1. We create an unowned dependency on the UINavigationController.
  2. In the start() method, we create the WelcomeViewController and push it onto the previously created navigationController‘s stack. Since we pass no data around, we return an Empty publisher.
  3. The navigateToMain() method is used by the WelcomeViewController to launch the main flow. Here, we create a MainCoordinator and coordinate to it. We will implement the MainCoordinator now.

With the welcome page done, let’s implement the main flow with the tab bar. First, let’s create a UITabBarController subclass called MainTabBarController:

Since it does not have any logic yet, its only responsibility is to keep the MainCoordinator alive. The AppTabBarController inheritance you see above is simply a class that provides the basic appearance of a tab bar controller.

Next, let’s create the actual MainCoordinator:

  1. As before, we create an unowned property navigationController.
  2. Inside the start() method, we create two UINavigationControllers and give them a UITabBarItem.
  3. Next, we initialize the MainTabBarController and set its modalPresentationStyle and viewControllers properties.
  4. We present the initialized MainTabBarController on the navigationController. As a result, the MainTabBarController will appear after a user taps on the button inside the Welcome screen.
  5. Initialize two coordinators, FirstCoordinator and SecondCoordinator.
  6. Coordinate with each of the coordinators since both of them represent screens of the tab bar controller.
  7. Finally, return an Empty publisher since we are not passing data yet.

This is the result of the steps mentioned above:

Now that the main flow is shown, let’s handle the data passing logic. For this reason, we have FirstCoordinator and ItemsCoordinator. Our goal is to present a list of items when a user taps on the “Select Item” button. Once an item is selected, the ItemsViewController will dismiss, and the item will be received inside the FirstViewController.

Before we create the ItemsCoordinatorlet’s see how FirstCoordinator is implemented:

  1. Just like before, we depend on the UINavigationController.
  2. The start() method is responsible for showing the FirstViewController on the screen. Since we do not pass the data from FirstViewController to anywhere else, this again results in Empty publisher.
  3. The presentItems() is responsible for navigating to the Items screen. As we can see, the method returns a Publisher containing an ItemsCoordinationResult. In short, this means when we select an item on the Items screen, the FirstCoordinator will obtain a value through this publisher. Once the value is received, the Items screen is dismissed.

Inside the FirstViewController, we fire this method after a button is tapped. Then, we observe and handle the result after the Items screen is dismissed:

To see what this ItemsCoordinationResult looks like, we move on to the ItemsCoordinator implementation:

  1. The ItemsCoordinationResult is an enum containing the item(String) case. In short, this enum represents possible outputs of the Items screen. If there were more options than just item selection, all we would do is add another case inside the enum.
  2. Now, since we are indeed passing the data, we are specifying ItemsCoordinationResult as the data type of the coordinator.
  3. We depend on the UINavigationController as before. In addition, we provide a PassthroughSubject and the view controller will fire once an item is selected.
  4. Inside the start() method, we display the ItemsViewController. We also return the previously created PassthroughSubjectsince this is the way to send a stream of events to the presenting coordinator.

And this is how the whole example app functions (the item selected will be printed in the console for simplicity):

The ready-to-use Swift package can be accessed on GitHub.

We have seen how the Coordinator design pattern makes it easier to make navigation inside the app more structured. If you want to learn more about the standard implementation of the pattern, I suggest you take a look at this article:

Thanks for reading!

Leave a Comment