An iOS navigation coordinator written in Swift 5.
There are a lot of implementations floating around the iOS community of using Coordinators to remove the burden of navigation from UIViewController
s. The Coordinator pattern is so broad, however, that there a lot of different interpretions of how to implement it.
This is my own take on the Coordinator pattern.
In my opinion, a Coordinator serves three main purposes:
- Handle the preparation, navigation between, and presentation of at least one - but often many - view controllers.
- Liase between different services, like a Networking Service, in order to pull business logic out of our view controllers.
- Optionally manage child Coordinators, in order to divy up responsibilities of complex navigation routes.
The coordinator sits above your view controllers and directs the flow of traffic via delegates.
For instance, say you had a view controller for browsing products to buy and wanted to tap into a product's detail, it would usually look something like this:
final class BrowseProductsViewController: UIViewController {
func onProductTapped(product: Product) {
let productDetailViewController = ProductDetailViewController(product: product)
navigationController?.pushViewController(productDetailViewController, animated: true)
}
}
With a Coordinator
, it would be broken up something like this:
// The delegate we'll use to talk to the Coordinator
protocol BrowseProductsViewControllerDelegate: class {
func browseProductsViewController(_ controller: BrowseProductsViewController, didTapProduct product: Product)
}
// Our revised view controller, now using the delegate
final class BrowseProductsViewController: UIViewController {
weak var delegate: BrowseProductsViewControllerDelegate?
func onProductTapped(product: Product) {
delegate?.browseProductsViewController(_ controller: self, didTapProduct product: product)
}
}
// The view controller's owning coordinator
final class BrowseProductsCoordinator: NavigationCoordinator {
// Any child coordinators this Coordinator is holding on to
var childCoordinators: [Coordinator] = []
// Our custom UINavigationController wrapper
var navigator: NavigatorType
// The view controller this Coordinator is managing
var rootViewController: BrowseProductsViewController
init(navigator: NavigatorType) {
self.navigator = navigator
self.rootViewController = BrowseProductsViewController()
}
func start() {
rootViewController.delegate = self
}
}
// With delegate conformance
extension BrowseProductsCoordinator: BrowseProductsViewControllerDelegate {
func browseProductsViewController(_ controller: BrowseProductsViewController, didTapProduct product: Product) {
let productDetailViewController = ProductDetailViewController(product: product)
// In a more complex situation, here's where the Coordinator could reference services,
// like fetching information from the network or the local data store,
// to prepare the view controller for presentation.
navigator.push(productDetailViewController, animated: true)
}
}
In the refactored code using a Coordinator
, we've taken away the view controller's knowledge of where it's been and where it's going. This ostensibly makes our view controller just a view, making it incredibly reusable, and pulls out routing and business logic to be handled by something else.
The Navigator
is a wrapper / proxy for a UINavigationController
, providing methods for the coordinator to push and pop view controllers on the navigation stack.
The reason for using Navigator
rather than a regular UINavigationController
is that it conforms to UINavigationControllerDelegate
, providing a callback when a controller is popped from the stack (like when a user swipes-to-go-back on a screen). These callbacks allow the Coordinator
to remove any child coordinators if needed, deallocating them from memory.
A single Coordinator could be used to route to multiple view controllers in a flattened hierarchy:
But it could also have one, or many, child coordinators which is responsible for a specific chunk of navigation:
It's a very flexible pattern, so use it how you determine best!
In this repo you'll see an example project of how someone might use the Coordinator pattern in an actual app.
In many examples I've found online, the pattern itself is often combined with ViewModels and/or using Reactive binding ("MVVM-C"). While these approaches can be helpful in solving certain problems, the Coordinator pattern itself can be used - and maybe better understood - without these added abstractions.
With that, my example project focuses exclusively on the relationship between Coordinators and ViewControllers, in an intentional effort to avoid the confusion of mixing together multiple patterns.
To see the example app, simply open Example/Coordinator-Example.xcodeproj
in Xcode and run the project.
The source for the example project is available on Cocoapods under the name SwiftCoordinator, if you want to experiment with this implementation yourself. Just add the following to your Podfile
:
pod 'SwiftCoordinator'
Thanks for checking out my take on coordinators -- I hope it's helpful if you're exploring the pattern to use in a project. if you have any suggestions or improvements, feel free to submit a PR!
I borrowed from a lot of examples around the community and want to thank / credit the following for inspiration and ideas:
Soroush Khanlou: Presenting Coordinators
Ian MacCallum: Coordinators, Routers, and Back Buttons
Paul Hudson: How to use the coordinator pattern in iOS apps