Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Big app with lots of screens

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Tree of coordinators

Slide 5

Slide 5 text

Reusing sub-flows

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Deep linking

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Pay

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

Settings Pay

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Need to remember to remove any references to this coordinator

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Revised version

Slide 18

Slide 18 text

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)

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Flip it around

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Settings Pay

Slide 27

Slide 27 text

Demo

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

No content