This document gives you suggestions on how to handle different scenarios in your project and how can you use Coordinator in each scenario.
This document is meant to give you ideas. Remember, however, Coordinator is flexible. Solve each problem in a way that fits your app best.
Let's say that you have a scenario where you are displaying a screen and after you are complete with that screen, you need to swap to another screen.
For example: You are shown a login screen if you are not logged into your GitHub account. You cannot navigate away from this login screen. After you login, you are taken to an issues list screen.
In this scenario, you can have a parent ViewController that you swap what ViewController to show inside of a coordinator.
import UIKit
/** A `UIViewController` which simply holds a child `UIViewController`. */
final class ViewControllerContainer: UIViewController {
func set(childViewController controller: UIViewController) {
children.forEach { (childController) in
childController.willMove(toParent: nil)
childController.view.removeFromSuperview()
childController.removeFromParent()
}
addChild(controller)
controller.didMove(toParent: self)
let childView = controller.view!
view.addSubview(childView, constraints: [childView.topAnchor.constraint(equalTo: view.topAnchor),
childView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
childView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
childView.trailingAnchor.constraint(equalTo: view.trailingAnchor)])
}
}
---
import SwiftCoordinator
class AppCoordinator: PresentationCoordinator {
var childCoordinators: [Coordinator] = []
var rootViewController = ViewControllerContainer()
private var loginViewController: LoginViewController?
private var issuesViewController: IssuesViewController?
init() {
if isUserLoggedIn {
goToIssues()
} else {
goToLogin()
}
}
func start() {
loginViewController?.delegate = self
}
private func goToLogin() {
loginViewController = LoginViewController()
rootViewController.set(loginViewController!)
}
private func goToIssues() {
issuesViewController = IssuesViewController()
rootViewController.set(issuesViewController!)
}
}
extension AppCoordinator: LoginViewControllerDelegate {
func userSuccessfullyLoggedIn(_ viewController: LoginViewController) {
self.goToIssues()
}
}
----
protocol LoginViewControllerDelegate: AnyObject {
func userSuccessfullyLoggedIn(_ viewController: LoginViewController)
}
class LoginViewController: UIViewController {
weak var delegate: LoginViewControllerDelegate?
...
func userSuccessfullyLoggedIn() {
delegate?.userSuccessfullyLoggedIn(self)
}
...
}
Note: We are using a
PresentationCoordinator
type of Coordinator object because we are not using aUINavigationController
. If you want to use aUINavigationController
to display yourUIViewControllers
, read that section of this doc on how to handle that.
Notice that we have 1 coordinator that we show 1 ViewController with. After the user logs into the app, we do not plan on showing the LoginViewController again. If we wanted to possibly go back, we would consider using a child coordinator and/or a navigation controller to be able to come back to this screen. But in this scenario, we don't want to navigate between the screens or go back to them. We want to force the app to showing 1 screen at a time.
When you want to use a UINavigtionController
within your app when using the Coordinator pattern, luckily Coordinator makes this quite easy.
import Foundation
import SwiftCoordinator
class IssuesCoordinator: NavigationCoordinator {
var childCoordinators: [Coordinator] = []
var navigator: NavigatorType
var rootViewController: UINavigationController
private let issuesListViewController: IssuesListViewController
init() {
issuesListViewController = IssuesListViewController()
let navigationController = UINavigationController(rootViewController: issuesListViewController)
self.navigator = Navigator(navigationController: navigationController)
self.rootViewController = navigationController
}
func start() {
issuesListViewController.delegate = self
}
}
extension IssuesCoordinator: LoginViewControllerDelegate {
func selectedIssue(_ viewController: LoginViewController, issueId: Int) {
let issueDetails = IssueDetailsViewController()
issueDetails.issueId = issueId
// `navigator` references the `Navigator` instance found in `NavigationCoordinator`
navigator.push(issueDetails, animated: true, onPoppedCompletion: nil)
}
}
----
protocol IssuesViewControllerDelegate: AnyObject {
func selectedIssue(_ viewController: LoginViewController, issueId: Int)
}
class IssuesViewController: UIViewController {
weak var delegate: IssuesViewControllerDelegate?
...
func selectedIssue(issueId: Int) {
delegate?.selectedIssue(self, issueId: issueId)
}
...
}
Coordinator comes with a Coordinator type, NavigationCoordinator
, that is designed to house a UINavigationController
. Using this Coordinator is similar to using a UINavigationController
with some added functionality such as being able to run code when a UIViewController
you pushed into the navigation controller is popped off. View all of the different functions you can call on your NavigationCoordinator
.
If you want to use a UITabBar
to navigate between the screens of your app like this:
...here is an idea on how to do this with Coordinator.
import Foundation
import UIKit
import SwiftCoordinator
final class AppCoordinator: PresentationCoordinator {
var childCoordinators: [Coordinator] = []
var rootViewController = AppTabBarViewController()
init(window: UIWindow) {
window.rootViewController = rootViewController
window.makeKeyAndVisible()
}
func start() {
}
}
---
import UIKit
final class AppTabBarViewController: UITabBarController {
/**
You must have a strong reference to the coordinators to keep them in memory.
*/
private let trendingCoordinator = TrendingCoordinator()
private let issuesCoordinator = IssuesCoordinator()
private var trendingViewController: UIViewController {
return self.trendingCoordinator.rootViewController
}
private var issuesViewController: UIViewController {
return self.issuesCoordinator.rootViewController
}
override func viewDidLoad() {
super.viewDidLoad()
trendingViewController.tabBarItem = UITabBarItem(...)
issuesViewController.tabBarItem = UITabBarItem(...)
trendingViewController.start()
issuesViewController.start()
self.viewControllers = [
trendingViewController,
issuesViewController
]
}
}
The Coordinator is pretty simple. It will just display the UITabBarController
. Coordinators are special here in that each tab of the UITabBarController
displays a different Coordinator! Great way to encapsulate the logic into separate re-usable Coordinators.
Let's now create these Coordinators.
import Foundation
import UIKit
import SwiftCoordinator
final class TrendingCoordinator: PresentationCoordinator {
var childCoordinators: [Coordinator] = []
let rootViewController = TrendingViewController()
init() {
}
func start() {
}
}
---
import Foundation
import UIKit
import SwiftCoordinator
import Foundation
import SwiftCoordinator
class IssuesCoordinator: NavigationCoordinator {
var childCoordinators: [Coordinator] = []
var navigator: NavigatorType
var rootViewController: UINavigationController
init() {
let navigationController = UINavigationController(rootViewController: IssuesListViewController())
self.navigator = Navigator(navigationController: navigationController)
self.rootViewController = navigationController
}
func start() {
}
}
If you have a UIViewController
that needs to open a webpage in a web browser, you could do that directly in your UIViewController
:
class ExampleViewController: UIViewController {
func openWebsite(_ url: URL) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
But that is not testable. There is a reason we use the Coordinator pattern in our apps! When you encounter scenarios when you need to navigate away from a screen, put that logic in your Coordinator
.
extension Coordinator {
func browse(to url: URL) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
---
protocol ExampleViewControllerDelegate: AnyObject {
func browseToWebsite(_ viewController: ExampleViewController, url: URL)
}
class ExampleViewController: UIViewController {
weak var delegate: ExampleViewControllerDelegate?
func openWebsite(_ url: URL) {
delegate?.browseToWebsite(self, url: url)
}
}
---
import Foundation
import UIKit
import SwiftCoordinator
final class ExampleCoordinator: PresentationCoordinator {
var childCoordinators: [Coordinator] = []
let rootViewController = ExampleViewController()
init() {
}
func start() {
rootViewController.delegate = self
}
}
extension ExampleCoordinator: ExampleViewControllerDelegate {
func browseToWebsite(_ viewController: ExampleViewController, url: URL) {
browse(to: url)
}
}
...or...if you would rather use the SaferiServices instead to navigate to your website, you can do this:
import SafariServices
extension ExampleCoordinator: ExampleViewControllerDelegate {
func browseToWebsite(_ viewController: ExampleViewController, url: URL) {
let controller = SFSafariViewController(url: url)
controller.delegate = self
rootViewController.present(controller, animated: true)
}
}
extension ExampleCoordinator: SFSafariViewControllerDelegate {
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
controller.dismiss(animated: true)
}
}
If you want to display an UIAlertController
within your app, here are some ideas. It does not matter if you wish to display the alert as a popup or a sheet. It does not matter if you wish to display an alert with many views or different options inside of it. The Coordinator can handle all of these scenarios when they are selected.
import Foundation
import UIKit
import SwiftCoordinator
final class ExampleCoordinator: PresentationCoordinator {
var childCoordinators: [Coordinator] = []
let rootViewController = ExampleViewController()
init() {
}
func start() {
rootViewController.delegate = self
}
}
extension ExampleCoordinator: ExampleViewControllerDelegate {
func showAlertToUser(_ viewController: ExampleViewController, retryHandler: () -> Void) {
let alertController = UIAlertController(title: nil, message: "", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Confirm", style: .default) { _ in
// Handle when the user selects "Confirm" in the Coordinator if you wish. Navigate somewhere else, start a child coordinator, whatever you wish.
})
alertController.addAction(UIAlertAction(title: "Retry", style: .default) { _ in
retryHandler()
})
alertController.addAction(UIAlertAction(title: "Cancel", style: .default) { _ in
alertController?.dismiss()
})
present(alertController, animated: true)
}
}
Modally present a UIViewController
:
import Foundation
import UIKit
import SwiftCoordinator
class TrendingCoordinator: NavigationCoordinator {
var childCoordinators: [Coordinator] = []
var navigator: NavigatorType
var rootViewController: UINavigationController
private let trendingViewController: TrendingViewController
init() {
self.trendingViewController = TrendingViewController()
let navigationController = UINavigationController(rootViewController: trendingViewController)
self.navigator = Navigator(navigationController: navigationController)
self.rootViewController = navigationController
}
func start() {
trendingViewController.delegate = self
}
}
extension TrendingCoordinator: TrendingViewControllerDelegate {
func didSelectRepo(_ repoId: Int) {
presentCoordinator(IssuesCoordinator(repoId: repoId, navigator: navigator), animated: true)
}
}
---
import UIKit
import SwiftCoordinator
class IssuesCoordinator: NavigationCoordinator {
var childCoordinators: [Coordinator] = []
var navigator: NavigatorType
var rootViewController: IssuesViewController
init(repoId: Int, navigator: NavigatorType) {
self.navigator = navigator
self.rootViewController = IssuesViewController()
rootViewController.repoId = repoId
}
func start() {
rootViewController.delegate = self
}
}
extension IssuesCoordinator: IssuesViewControllerDelegate {
// When the "Back" button is pressed in the modally presented ViewController
func cancel(_ viewController: IssuesViewController) {
dismissCoordinator(self, animated: true)
}
}
We are passing in the navigtion controller from the TrendingCoordinator
into the IssuesCoordinator
. This allows your IssuesCoordinator
to dismiss itself when it's flow is complete.
When you begin using Coordinators, you are no longer passing arguments directly to your destination Views. The Coordinator object now sits in between the source and the destination view.
Luckily, passing arguments is quite easy. The Coordinator can take in arguments in the constructor if the navigation flow requires some arguments to function. The Coordinator can also take in arguments when actions are taken from delegate functions.
import UIKit
import SwiftCoordinator
class IssuesCoordinator: NavigationCoordinator {
var childCoordinators: [Coordinator] = []
var navigator: NavigatorType
var rootViewController: IssuesViewController
init(repoId: Int) {
self.rootViewController = IssuesViewController()
rootViewController.repoId = repoId
}
func start() {
}
}
If your root view controller of your Coordinator cannot function without some arguments, for example, add argument to the Coordinator constructor that other Coordinators must then use when they want to display the child Coordinator.
presentCoordinator(IssuesCoordinator(repoId: repoId), animated: true)
Coordinators are the objects that act as the delegates for UIViewController
s. As delegates, they can receive arguments and pass them onto the UIViewController
:
extension TrendingCoordinator: TrendingViewControllerDelegate {
// When a user performs an action in a UIViewController, this delegate function is called.
func didSelectRepo(_ repoId: Int) {
// It is the Coordinator's responsibility to pass that on to the destination.
// presentCoordinator() is a function of the Coordinator that you can use to display a UIViewController modally.
presentCoordinator(IssuesCoordinator(repoId: repoId, navigator: navigator), animated: true)
}
}
As explained with this graphic in the Coordinator library README, you have the option of Coordinators presenting child coordinators to break up the flow of your app.
What is neat about this is that it allows your all of your Coordinators to be more re-usable. Any Coordinator can be displayed as a child within another Coordinator.
This also makes your code more readable and maintainable. Instead of having 1 super long and complex Coordinator that contains the logic of your entire app, you can break them up into smaller Coordinators.
Let's say that your app requires users login to their account and have a username assigned to them. You can create a LoginCoordinator that contains all of the logic inside of it to (1) ask the user to enter their email address in and then (2) ask the user for a username if they have not assigned one already or skip asking them if they have already done so. When you contain all of the login workflow within 1 Coordinator, you can then add login to your app in many places with ease!
To have Coordinators display other Coordinators, there are some tips to keep in mind:
- Have Coordinators talk to each other through delegates.
protocol LoginCoordinatorDelegate: class {
func loginCoordinatorDidFinish(_ coordinator: LoginCoordinator)
}
final class LoginCoordinator: NavigationCoordinator {
weak var delegate: LoginCoordinatorDelegate?
var childCoordinators: [Coordinator] = []
var navigator: NavigatorType
var rootViewController: UINavigationController
init() {
// Create LoginViewController. Have this Coordinator be the delegate for the ViewControllers in the login workflow and switch between the screens however you wish using the scenarios outlined in this document.
// After you are all done logging in, call `delegate?.loginCoordinatorDidFinish(self)` to tell the parent Coordinator you are done!
}
func start() {
}
}
---
final class ExampleCoordinator: PresentationCoordinator {
var childCoordinators: [Coordinator] = []
var rootViewController: ExampleViewController
init() {
rootViewController = ExampleViewController()
}
func start() {
rootViewController?.delegate = self
}
}
extension ExampleCoordinator: ExampleViewControllerDelegate {
func askUserToLogin() {
let loginCoordinator = LoginCoordinator()
loginCoordinator.delegate = self
presentCoordinator(loginCoordinator, animated: true)
}
}
extension AppCoordinator: LoginCoordinatorDelegate {
func loginCoordinatorDidFinish(_ coordinator: LoginCoordinator) {
dismissCoordinator(coordinator, animated: true)
// Your user is now logged in! You have no idea if the user previously had a username assigned to their account or they were asked for one, but you don't care. You simply know that if this function is called, the user is fully logged in and you can then use their username within the app.
}
}
...images for the post. Nothing special, just a place to host the images for the gist.
Images
     