Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Coordinators and deep linking

rechsteiner
February 13, 2020

Coordinators and deep linking

How we handle navigation inside the mobile bank at DNB

rechsteiner

February 13, 2020
Tweet

More Decks by rechsteiner

Other Decks in Programming

Transcript

  1. Coordinators and deep linking
    How we handle navigation inside the mobile bank

    View Slide

  2. Big app with lots of screens

    View Slide

  3. • Coordinators are responsible for the presentation flow between view
    controllers.
    • View controllers have no knowledge about the context they are being used.
    The coordinator pattern
    protocol Coordinator: class {
    var childCoordinators: [Coordinator] { get set }
    }

    View Slide

  4. Tree of coordinators

    View Slide

  5. Reusing sub-flows

    View Slide

  6. class AppCoordinator: Coordinator {
    func open(deepLink: DeepLink, animated: Bool) {
    if authService.isAuthenticated {
    tabBarCoordinator.open(deepLink: deepLink, animated: animated)
    } else {
    showAuthCoordinator()
    }
    }
    }

    View Slide

  7. Deep linking

    View Slide

  8. enum DeepLink {
    case pay
    case transfer
    case pendingPayments
    case accountDetail(id: String)
    }
    protocol Coordinator: class {
    var childCoordinators: [Coordinator] { get set }
    func open(deepLink: DeepLink, animated: Bool)
    }

    View Slide

  9. Pay

    View Slide

  10. class AppCoordinator: Coordinator {
    ...
    func open(deepLink: DeepLink, animated: Bool) {
    if authService.isAuthenticated {
    tabBarCoordinator.open(deepLink: deepLink, animated: animated)
    } else {
    showAuthCoordinator()
    }
    }
    }

    View Slide

  11. Problem #1
    • Each new deep link requires you to update the entire tree of coordinators

    View Slide

  12. View Slide

  13. Settings Pay

    View Slide

  14. Problem #2
    • Need to manually remove references to coordinators when navigating
    backwards in a navigation controller

    View Slide

  15. Need to remember to remove any
    references to this coordinator

    View Slide

  16. extension Coordinator: UINavigationControllerDelegate {
    func navigationController(
    navigationController: UINavigationController,
    didShowViewController viewController: UIViewController,
    animated: Bool
    ) {
    guard
    let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from),
    !navigationController.viewControllers.contains(fromViewController) else {
    return
    }
    if fromViewController is HomeViewController {
    homeCoordinator = nil
    }
    }
    }
    Back button problem

    View Slide

  17. Revised version

    View Slide

  18. Automatically deallocate coordinators
    open class NavigationCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
    weak var parentCoordinator: NavigationCoordinator?
    var rootViewController: UIViewController
    var navigationController: UINavigationController
    var childCoordinators: [Coordinator]
    var childViewControllers: [WeakRef]
    init(rootViewController: UIViewController, navigationController: UINavigationController) {
    self.childCoordinators = []
    self.childViewControllers = []
    self.rootViewController = rootViewController
    self.navigationController = navigationController
    super.init()
    }
    ...
    public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
    guard let toViewController = navigationController.transitionCoordinator?.viewController(forKey: .to) else { return }
    removeIfNeeded(coordinator: self, toViewController: toViewController)
    }
    private func contains(coordinator: NavigationCoordinator, viewController: UIViewController) -> Bool {
    return coordinator.childViewControllers.contains(where: { $0.value === viewController })
    }
    func removeIfNeeded(coordinator: NavigationCoordinator, toViewController: UIViewController) {
    if contains(coordinator: coordinator, viewController: toViewController) {
    return
    }
    else if let parentCoordinator = coordinator.parentCoordinator {
    parentCoordinator.remove(coordinator: coordinator)
    parentCoordinator.set(delegate: parentCoordinator)
    removeIfNeededcoordinator: parentCoordinator, toViewController: toViewController)

    View Slide

  19. Example
    final class AccountDetailCoordinator: NavigationCoordinator {
    }
    func select() {
    let viewController = AccountConditionsViewController()
    viewController.coordinator = self
    push(viewController: viewController, animated: true)
    }
    func selectConditions() {
    push(coordinator: AccountDetailCoordinator(navigationController))
    }

    View Slide

  20. Problem #3
    • View controller could not use UINavigationControllerDelegate
    • Basically reimplementing stuff we get for free in UIKit

    View Slide

  21. Back to basics
    • Just use view controllers directly?
    • Can get the separation using parent/child view controllers
    • Hard to reuse sub-flows in navigation controllers

    View Slide

  22. Flip it around

    View Slide

  23. What about deep linking?
    • We no longer have a tree of coordinators
    • Traverse the view controller hierarchy instead
    • Need to associate a given view controller with something

    View Slide

  24. struct DeepLink {
    let destination: Destination
    let path: [Destination]
    }
    enum Destination {
    case home
    case payments
    case spending
    case profile
    case pay
    case transfer
    case pendingPayments
    case accountDetail(id: String)
    }

    View Slide

  25. DeepLinkable
    protocol DeepLinkable {
    var destination: Destination { get }
    var coordinator: Coordinator { get }
    }
    final class AccountDetailViewController: UIViewController, DeepLinkable {
    let destination: Destination
    let coordinator: HomeCoordinator
    init(id: String, coordinator: HomeCoordinator) {
    self.coordinator = coordinator
    self.destination = .accountDetail(id: id)
    super.init(nibName: nil, bundle: nil)
    }
    }

    View Slide

  26. Settings Pay

    View Slide

  27. Demo

    View Slide

  28. Tips for deep linking
    • Usually no need for coordinators
    • Each view controller should fetch data in isolation
    init(model: Account)
    init(id: String)

    View Slide

  29. View Slide