The UIKit framework makes it trivial to navigate between view controllers. You can perform a segue if you use storyboards or you can programmatically navigate from one view controller to the next. This is convenient, but there's also a cost. If you use the API the UIKit framework offers you to navigate between view controllers, then you have less control and flexibility. This can become a significant problem as your project grows.
This series zooms in on the coordinator pattern, an alternative solution that works around the limitations of the UIKit framework. The coordinator pattern isn't new. I stumbled upon the pattern several years ago on Soroush Khanlou's blog. He introduced the coordinator pattern in 2015 in a series of excellent articles, explaining the problems most projects suffer from and offering a promising solution.
The coordinator pattern is easy to adopt and is compatible with the Model-View-Controller and Model-View-ViewModel patterns. Before I show you how to implement the coordinator pattern, you need to understand what problem it solves.
An Example
To illustrate the problem we solve in this series, I've created a simple application, Quotes. Quotes shows the user a table view with quotes. Tapping a quote takes the user to a detail view.
The user can modify the font size and the font weight of the quotes in the application's settings view.
Even though Quotes is a typical project powered by the UIKit framework, it suffers from a few problems that may not be immediately obvious. The problems are inherent to UIKit and its API.
Storyboards and Segues
Open Main.storyboard. The storyboard contains four view controllers, a navigation controller, the quotes and quote view controllers, and the settings view controller. The quotes view controller is the root view controller of the navigation controller.
The quotes and quote view controllers are linked through a segue. When the user taps a row in the table view, the user is taken to a detail view. The settings view controller doesn't have any segues. We programmatically navigate from the quotes view controller to the settings view controller. For that to work, the settings view controller has a storyboard identifier.
Apple introduced storyboards several years ago and many developers use it in their projects. Storyboards have a number of compelling benefits. They make navigation straightforward and visualize the flow of your application. These benefits come at a cost, though. They slowly turn into drawbacks as the complexity of your project increases.
Pushing and Presenting View Controllers
Open QuotesViewController.swift and navigate to the settings(_:)
method. This method is invoked when the user taps the settings button in the top right of the quotes view controller. The implementation is straightforward. The quotes view controller loads the main storyboard and instantiates the settings view controller. The quotes view controller presents the settings view controller modally by invoking its present(_:animated:completion:)
method, passing in the settings view controller as the first argument.
@IBAction func settings(_ sender: Any) {
guard let settingsViewController = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: SettingsViewController.storyboardIdentifier) as? SettingsViewController else {
fatalError("Unable to Instantiate Settings View Controller")
}
// Present Settings View Controller
present(settingsViewController, animated: true)
}
There doesn't seem to be anything wrong with the implementation of the settings(_:)
method. It uses an API of the UIKit framework to navigate from the quotes view controller to the settings view controller. What is the problem?
What Is the Problem?
Problem 1
The project is simple and yet there are a few problems I would like to resolve. Let's start with the storyboard. The quotes and quote view controllers are linked through a segue. This is what segues are designed for, but it compromises the reusability of the view controllers. The problem isn't limited to the storyboard.
Open QuotesViewController.swift and navigate to the prepare(for:sender:)
method. The quote view controller needs to know which quote it should display to the user. The quotes view controller needs to pass the user's selection to the quote view controller. It does this in the prepare(for:sender:)
method.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else {
return
}
if identifier == "Quote" {
guard let destination = segue.destination as? QuoteViewController else {
return
}
guard let indexPath = tableView.indexPathForSelectedRow else {
return
}
// Fetch Quote
let quote = quotes[indexPath.row]
// Configure Destination
destination.quote = quote
}
}
What is the problem? The QuotesViewController
class knows about the QuoteViewController
class. The view controllers are tightly coupled to make it possible to navigate from the quotes view controller to the quote view controller. This is a red flag we should address.
Problem 2
Tapping the settings button in the top right of the quotes view controller, takes the user to the settings view controller. The implementation of the settings(_:)
method is a typical example of using UIKit's API to instantiate a view controller and presenting it to the user. The implementation has a few problems, though.
The quotes view controller knows about the SettingsViewController
class and it knows how to instantiate an instance of the SettingsViewController
class. The view controllers are tightly coupled to make it possible to navigate from the quotes view controller to the settings view controller. The quotes view controller presents the settings view controller to the user. In other words, the quotes view controller is put in charge of navigating the application. This is another red flag.
Problem 3
There's another, more subtle problem. The user can tap the Done button in the application's settings view to dismiss the settings view controller. The implementation of the dismiss(_:)
method should look familiar. The settings view controller invokes its dismiss(animated:)
method to dismiss itself. In other words, the settings view controller is responsible for its own dismissal.
@IBAction func dismiss(_ sender: Any) {
// Dismiss View Controller
dismiss(animated: true)
}
You may be wondering why that's an issue. The answer is simple. A view controller shouldn't be in charge of navigation, including dismissing itself.
Massive View Controllers
I discussed fat or massive view controllers at length in Mastering MVVM With Swift. A view controller, as its name implies, should control a view and that should be its only responsibility. Any other tasks should be delegated to other objects and that includes navigation.
A view controller shouldn't need to know about other view controllers and it shouldn't be in charge of navigating between view controllers. It tightly couples the view controllers and limits their reusability.
It's Not You, It’s UIKit
It's important to understand that the UIKit framework expects you to tightly couple view controllers. A view controller is expected to be in charge of navigation. That's what the present(_:animated:completion:)
method was designed for. The UIKit framework was built with the Model-View-Controller pattern in mind. The MVC pattern defines models, views, and controllers. As we discussed in Mastering MVVM With Swift, the controller is put in charge of many tasks, including navigation.
What Is the Coordinator Pattern?
The solution is simpler than you might think and you won't need to spend hours or days learning yet another design pattern. This series outlines how the coordinator pattern can help resolve the problems I mentioned earlier. A coordinator coordinates the flow of the application. View controllers are no longer involved in navigation. That task is handled by the coordinator.
In this series, we explore the MVC-C and MVVM-C patterns. The trailing C stands for Coordinator. The coordinator pattern introduces a new type of object to the application architecture to simplify the view controllers of the project. The result is flexibility and increased reusability.
What's Next?
In the next episode, we refactor Quotes by introducing the coordinator pattern and I show you how storyboards can be used without tightly coupling view controllers.