Navigate and pass data between screens reactively
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
- How to use
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
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.
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
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
viewcontroller 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:
- Add an
unowneddependency on the application’s
- Provide an initializer.
- Implement the
start()method by creating a
UINavigationControllerand setting it as the
rootViewControllerof 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:
- We add a strong dependency on the
- In the
scene(_scene:willConnectTo:...)method, initialize 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:
- We add a dependency on the yet-to-be implemented
- In the
viewDidLoad()we launch binding and UI setup methods.
- In the
bindButtonToCoordinator()method, we create a publisher for button taps, on each of which we fire the coordinator’s
- We create and layout a
UIButtonin a standard way.
Let’s implement the
- We create an
unowneddependency on the
- In the
start()method, we create the
WelcomeViewControllerand push it onto the previously created
navigationController‘s stack. Since we pass no data around, we return an
navigateToMain()method is used by the
WelcomeViewControllerto launch the main flow. Here, we create a
MainCoordinatorand coordinate to it. We will implement the
With the welcome page done, let’s implement the main flow with the tab bar. First, let’s create a
UITabBarController subclass called
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
- As before, we create an
- Inside the
start()method, we create two
UINavigationControllers and give them a
- Next, we initialize the
MainTabBarControllerand set its
- We present the initialized
navigationController. As a result, the
MainTabBarControllerwill appear after a user taps on the button inside the Welcome screen.
- Initialize two coordinators,
- Coordinate with each of the coordinators since both of them represent screens of the tab bar controller.
- Finally, return an
Emptypublisher 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
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
Before we create the
ItemsCoordinatorlet’s see how
FirstCoordinator is implemented:
- Just like before, we depend on the
start()method is responsible for showing the
FirstViewControlleron the screen. Since we do not pass the data from
FirstViewControllerto anywhere else, this again results in
presentItems()is responsible for navigating to the Items screen. As we can see, the method returns a
ItemsCoordinationResult. In short, this means when we select an item on the Items screen, the
FirstCoordinatorwill obtain a value through this publisher. Once the value is received, the Items screen is dismissed.
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
ItemsCoordinationResultis 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.
- Now, since we are indeed passing the data, we are specifying
ItemsCoordinationResultas the data type of the coordinator.
- We depend on the
UINavigationControlleras before. In addition, we provide a
viewcontroller will fire once an item is selected.
- 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!