Mastering Navigation With Coordinators

Navigating With a Coordinator

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy

With the foundation in place, it's time to use the coordinator to navigate between view controllers. Remember that the goal is to put the coordinator in charge of (1) instantiating view controllers and (2) navigating between view controllers. This means that we need to make a few changes.

First, we remove any segues from the main storyboard. Second, the main storyboard needs to assign a storyboard identifier to every view controller it contains. By assigning a storyboard identifier to a view controller, the storyboard is able to instantiate that view controller. Third, we move the instantiation of view controllers to the coordinator. View controllers should not be responsible for instantiating other view controllers. Fourth, the coordinator is in charge of navigating between view controllers. A view controller should not know how to show or hide view controllers, including itself.

Updating the Storyboard

Let's start by updating the storyboard. We need to make two changes. Open Main.storyboard and remove the segue connecting the quotes view controller to the quote view controller. We no longer need it.

Removing Segues

The compiler warns us that the quote view controller is unreachable because it has no entry points and the storyboard hasn't assigned it a storyboard identifier. Let's fix that warning. Select the quote view controller, open the Identity Inspector on the right, and set Storyboard ID to QuoteViewController.

Assigning a Storyboard Identifier

Before we move on, we need to conform the QuoteViewController class to the Storyboardable protocol. In the previous episode, you learned that that's a trivial task. Open QuoteViewController.swift and conform the QuoteViewController class to the Storyboardable protocol. We don't need to implement any properties or methods.

import UIKit

class QuoteViewController: UIViewController, Storyboardable {

    ...

}

Showing the Quote View Controller

By removing the segue connecting the quotes view controller to the quote view controller it's no longer possible to navigate from the quotes view controller to the quote view controller. Let's fix that. We need to notify the coordinator when the user selects a quote from the table view. We have several options to notify the coordinator. Delegation is a common option, but I prefer the use of closures. The resulting API is more intuitive and less complex. Let me show you how it works.

Open QuotesViewController.swift and define a property with name didShowQuote. The property is of an optional type, a closure that accepts a Quote instance as its only argument.

import UIKit

class QuotesViewController: UIViewController, Storyboardable {

    // MARK: - Properties

    var didShowQuote: ((Quote) -> Void)?

    ...

}

The quotes view controller invokes the didShowQuote handler when the user selects a quote from the table view. Navigate to the tableView(_:didSelectRowAt:) method, a method of the UITableViewDelegate protocol. We fetch the quote that corresponds with the user's selection and pass it to the didShowQuote handler.

extension QuotesViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        // Fetch Quote
        let quote = quotes[indexPath.row]

        // Invoke Handler
        didShowQuote?(quote)
    }

}

We need to install the didShowQuote handler in the coordinator. Open AppCoordinator.swift and navigate to the showQuotes() method. We assign a closure to the didShowQuote property of the QuotesViewController instance. In the body of the closure, we invoke a helper method, showQuote(_:). The helper method accepts the Quote instance as its only argument.

private func showQuotes() {
    // Initialize Quotes View Controller
    let quotesViewController = QuotesViewController.instantiate()

    // Configure Quotes View Controller
    quotesViewController.didShowQuote = { [weak self] (quote) in
        self?.showQuote(quote)
    }

    // Push Quotes View Controller Onto Navigation Stack
    navigationController.pushViewController(quotesViewController, animated: true)
}

The implementation of the showQuote(_:) method is straightforward. We initialize an instance of the QuoteViewController class by invoking the instantiate() method on the QuoteViewController class. We assign the Quote instance to the quote property of the quote view controller. To present the quote view controller to the user, we push it onto the navigation stack of the navigation controller of the coordinator.

private func showQuote(_ quote: Quote) {
    // Initialize Quote View Controller
    let quoteViewController = QuoteViewController.instantiate()

    // Configure Quote View Controller
    quoteViewController.quote = quote

    // Push Quote View Controller Onto Navigation Stack
    navigationController.pushViewController(quoteViewController, animated: true)
}

Before we give it a try, open QuotesViewController.swift and remove the prepare(for:sender:) method. The QuotesViewController class no longer needs to handle segues.

Build and run the application and select a quote from the table view. The changes we made shouldn't affect the user experience in any way.

Showing the Settings View Controller

To show the settings view controller, we follow the same pattern. Open QuotesViewController.swift and define a property with name didShowSettings. The property is of an optional type, a closure that accepts no arguments.

import UIKit

class QuotesViewController: UIViewController, Storyboardable {

    // MARK: - Properties

    var didShowQuote: ((Quote) -> Void)?

    // MARK: -

    var didShowSettings: (() -> Void)?

    ...

}

We invoke the didShowSettings handler in the settings(_:) method. The quotes view controller no longer instantiates and presents a SettingsViewController instance. It notifies the coordinator by invoking the didShowSettings handler.

@IBAction func settings(_ sender: Any) {
    // Invoke Handler
    didShowSettings?()
}

It's the coordinator that instantiates and presents a SettingsViewController instance. Open AppCoordinator.swift and navigate to the showQuotes() method. We assign a closure to the didShowSettings property of the QuotesViewController instance. In the body of the closure, we invoke a helper method, showSettings().

private func showQuotes() {
    // Initialize Quotes View Controller
    let quotesViewController = QuotesViewController.instantiate()

    // Configure Quotes View Controller
    quotesViewController.didShowSettings = { [weak self] in
        self?.showSettings()
    }

    quotesViewController.didShowQuote = { [weak self] (quote) in
        self?.showQuote(quote)
    }

    // Push Quotes View Controller Onto Navigation Stack
    navigationController.pushViewController(quotesViewController, animated: true)
}

The implementation of the showSettings() method should look familiar. We initialize an instance of the SettingsViewController class by invoking the instantiate() method on the SettingsViewController class. To present the settings view controller, we invoke the present(_:animated:completion:) method on the navigation controller of the coordinator, passing in the SettingsViewController instance.

private func showSettings() {
    // Initialize Settings View Controller
    let settingsViewController = SettingsViewController.instantiate()

    // Present Settings View Controller
    navigationController.present(settingsViewController, animated: true)
}

Build and run the application. Tap the Settings button in the top right to bring up the application's settings view.

Hiding the Settings View Controller

The coordinator is in charge of instantiating and showing the view controllers of the project. But we're not finished. There's one more change we need to make. The settings view controller dismisses itself when the user taps the Done button in the top right. Remember that it's the coordinator's responsibility to show and hide the view controllers of the project. Hiding the settings view controller follows a similar pattern.

Open SettingsViewController.swift and define a variable property with name didHide. The property is of an optional type, a closure that accepts no arguments.

import UIKit

class SettingsViewController: UIViewController, Storyboardable {

    // MARK: - Properties

    var didHide: (() -> Void)?

    ...

}

Navigate to the dismiss(_:) method. The settings view controller no longer invokes the dismiss(animated:completion:) method defined by the UIViewController class. It notifies the coordinator by invoking the didHide handler.

@IBAction func dismiss(_ sender: Any) {
    // Invoke Handler
    didHide?()
}

Open AppCoordinator.swift and navigate to the showSettings() method. We assign a closure to the didHide property of the SettingsViewController instance. In the body of the closure, we call the dismiss(animated:completion:) method on the navigation controller of the coordinator.

private func showSettings() {
    // Initialize Settings View Controller
    let settingsViewController = SettingsViewController.instantiate()

    // Configure Settings View Controller
    settingsViewController.didHide = { [weak self] in
        self?.navigationController.dismiss(animated: true)
    }

    // Present Settings View Controller
    navigationController.present(settingsViewController, animated: true)
}

Build and run the application. Tap the Settings button in the top right to show the application's settings view. Tap the Done button to hide the application's settings view.

What Did We Gain?

In this episode, the advantages of the coordinator pattern have become more apparent. What did we gain by adopting the coordinator pattern?

First, view controllers are no longer responsible for instantiating other view controllers. The view controllers of the project are decoupled.

Second, the flow of the application isn't defined in a storyboard or dictated by the view controllers. It's the coordinator that defines the flow of the application. Only the coordinator shows and hides view controllers.

Third, the reusability of the view controllers has increased. We no longer use segues and view controllers are no longer tightly coupled. This makes it trivial to reuse any of the view controllers of the project. While this may not be immediately obvious in a simple project like the one we explored in this episode, I can assure you that larger or more complex projects that reuse view controllers in several locations greatly benefit from the coordinator pattern.

Delegation and Notifications

In this episode, we used closures to notify the coordinator when the user performs an action. Closures aren't the only option you have to notify the coordinator. Delegation is a viable alternative. The use of closures in this episode is a personal choice. The resulting API is concise, intuitive, and elegant.

Delegation is a bit more involved. You define a protocol and the methods of the protocol. The coordinator conforms to the protocol by implementing its methods. This results in more code in several locations and an API that is less elegant. But know that the result is similar.

I don't recommend using notifications to notify the coordinator. Notifications are designed for communication between objects that don't have a clearly defined relationship. The UIApplication.didBecomeActiveNotification notification illustrates this. The UIApplication instance posts the notification when it becomes active. It doesn't know which objects listen for the notification and it doesn't care. Any object interested in the notification can listen for the notification. There is no one-to-one relationship.

Delegation and closures are a good fit if the objects that are communicating have a clearly defined relationship, a one-to-one relationship. The coordinator is the receiver of the message and the view controller is the sender of the message. Know that notifications would work fine, but they're not a good fit for the coordinator pattern.

What's Next?

I hope that the past few episodes have sparked your interest in the coordinator pattern. It's easy to implement and it comes with little overhead. The project doesn't depend on a third party library and the learning curve is gentle. I'm sure you agree that the coordinator pattern is easy to adopt. In the next episode of this series, we continue exploring the coordinator pattern and its possibilities.

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy
Next Episode "Adding Flexibility and Dynamism"