Save 37% off PRO during our Black Friday Sale! »

Coordinators and deep linking

9c7c5ab5929ef286ec2b8fe3e8256ea0?s=47 rechsteiner
February 13, 2020

Coordinators and deep linking

How we handle navigation inside the mobile bank at DNB

9c7c5ab5929ef286ec2b8fe3e8256ea0?s=128

rechsteiner

February 13, 2020
Tweet

Transcript

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

    mobile bank
  2. Big app with lots of screens

  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 } }
  4. Tree of coordinators

  5. Reusing sub-flows

  6. class AppCoordinator: Coordinator { func open(deepLink: DeepLink, animated: Bool) {

    if authService.isAuthenticated { tabBarCoordinator.open(deepLink: deepLink, animated: animated) } else { showAuthCoordinator() } } }
  7. Deep linking

  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) }
  9. Pay

  10. class AppCoordinator: Coordinator { ... func open(deepLink: DeepLink, animated: Bool)

    { if authService.isAuthenticated { tabBarCoordinator.open(deepLink: deepLink, animated: animated) } else { showAuthCoordinator() } } }
  11. Problem #1 • Each new deep link requires you to

    update the entire tree of coordinators
  12. None
  13. Settings Pay

  14. Problem #2 • Need to manually remove references to coordinators

    when navigating backwards in a navigation controller
  15. Need to remember to remove any references to this coordinator

  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
  17. Revised version

  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<UIViewController>] 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)
  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)) }
  20. Problem #3 • View controller could not use UINavigationControllerDelegate •

    Basically reimplementing stuff we get for free in UIKit
  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
  22. Flip it around

  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
  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) }
  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) } }
  26. Settings Pay

  27. Demo

  28. Tips for deep linking • Usually no need for coordinators

    • Each view controller should fetch data in isolation init(model: Account) init(id: String)
  29. None