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

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

More Decks by rechsteiner

Other Decks in Programming

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