Skip to content

Instantly share code, notes, and snippets.

@levibostian
Last active July 31, 2020 20:45
Show Gist options
  • Save levibostian/08f74511c347e695b8448ff6b2fc02a8 to your computer and use it in GitHub Desktop.
Save levibostian/08f74511c347e695b8448ff6b2fc02a8 to your computer and use it in GitHub Desktop.
Coordinator pattern scenarios. For project: https://github.com/daveneff/Coordinator

Scenarios

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.

Swap UIViewControllers

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. swap_vcs

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 a UINavigationController. If you want to use a UINavigationController to display your UIViewControllers, 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.

UINavigationController

When you want to use a UINavigtionController within your app when using the Coordinator pattern, luckily Coordinator makes this quite easy.

nav_controller

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.

UITabBar

If you want to use a UITabBar to navigate between the screens of your app like this: tabbar

...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() {
    }
}

Open a URL to view in a browser

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)
    }
}

UIAlertController

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.

actioncontroller

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 presenting and dismissing a UIViewController

Modally present a UIViewController:

modally

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.

Passing arguments

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 UIViewControllers. 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)
    }
}

Child coordinators

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.

child

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!

child

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. 
    }
}
@levibostian
Copy link
Author

levibostian commented Mar 16, 2020

...images for the post. Nothing special, just a place to host the images for the gist.

Images
![swap_vcs](https://user-images.githubusercontent.com/2041082/76799098-cede0b80-67c8-11ea-80a1-85de08499a22.png) ![nav_controller](https://user-images.githubusercontent.com/2041082/76799859-49f3f180-67ca-11ea-8add-fbab7de1f80a.png) ![tabbar](https://user-images.githubusercontent.com/2041082/76801228-1feffe80-67cd-11ea-9393-fa27ca845c5f.png) ![actioncontroller](https://user-images.githubusercontent.com/2041082/76803642-38164c80-67d2-11ea-9ba9-ec90a0d173ef.png) ![modally](https://user-images.githubusercontent.com/2041082/76804315-db1b9600-67d3-11ea-94b6-59023b081bed.png) ![child](https://user-images.githubusercontent.com/2041082/76805431-aceb8580-67d6-11ea-8356-8f8e570070e4.png)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment