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

Андрей Рычков: How to deal with navigation

Андрей Рычков: How to deal with navigation

CocoaHeads

May 07, 2018
Tweet

More Decks by CocoaHeads

Other Decks in Technology

Transcript

  1. Andrei Rychkov H O W TO D E A L

    W I T H N AV I G AT I O N
  2. A B O U T M E • 5+ years

    in iOS development • Worked on projects in various areas • Now – Lead iOS Developer at FBS !2
  3. T H E P R O B L E M

    • Hard to maintain • Hard to merge • Strong relations between controllers !6
  4. !7 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch

    segue.destination { case let dateController as DateViewController: dateController.date = Date() case let emailController as EmailViewController: emailController.email = "[email protected]" default: break } } P R E PA R E F O R S E G U E
  5. func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() ->

    Swift.Void)? = nil) func pushViewController(_ viewController: UIViewController, animated: Bool) !8 P R E S E N T / P U S H
  6. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { var controllerToPush:

    UIViewController let row = sections[indexPath.section].rows[indexPath.row] switch row { case let .emailDetails(email, server): controllerToPush = EmailDetailsViewController(email: email, server: server) case .passwordChange: controllerToPush = PasswordChangeViewController() } navigationController?.pushViewController(controllerToPush, animated: true) } !9 T Y P I C A L TA B L E V I E W
  7. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { var controllerToPush:

    UIViewController let row = sections[indexPath.section].rows[indexPath.row] switch row { case let .emailDetails(email, server): controllerToPush = EmailDetailsViewController(email: email, server: server) case .passwordChange: controllerToPush = PasswordChangeViewController() } navigationController?.pushViewController(controllerToPush, animated: true) } !10 T Y P I C A L TA B L E V I E W
  8. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { var controllerToPush:

    UIViewController let row = sections[indexPath.section].rows[indexPath.row] switch row { case let .emailDetails(email, server): controllerToPush = EmailDetailsViewController(email: email, server: server) case .passwordChange: controllerToPush = PasswordChangeViewController() } navigationController?.pushViewController(controllerToPush, animated: true) } !11 T Y P I C A L TA B L E V I E W
  9. !12 B U S I N E S S C

    H A N G E S N AV I G AT I O N
  10. final class Router { private weak var window: UIWindow? private

    var state: State = .auth private var navigationController: UINavigationController? { return sideMenuController.rootViewController } func showPayments(with operationType: OperationType) { let controller = SystemsListViewController(operationType: operationType) navigationController?.pushViewController(controller, animated: true) } } !13 R O U T E R W I L L C H A N G E E V E RY T H I N G
  11. !14

  12. T H E P R O B L E M

    • Router – GOD object • It controls all screens navigation • Has many different states !15
  13. !16 N E W P R O J E C

    T – N E W L I F E
  14. C O N D I T I O N S

    • Several applications • Same flows • Flows must be customisable • Business logic often changes • Can’t easily change navigation in screen’s code !17
  15. A U T H F L O W !20 LOGIN


    /
 SIGNUP HAS ACCOUNT ?
  16. A U T H F L O W !21 LOGIN


    /
 SIGNUP MAIN
 FLOW HAS ACCOUNT ? Yes
  17. A U T H F L O W !22 LOGIN


    /
 SIGNUP CREATE ACCOUNT MAIN
 FLOW HAS ACCOUNT ? Yes No CHOOSE
 ACCOUNT
 TYPE
  18. M E E T C O O R D I

    N AT O R !
  19. M A I N P O I N T S

    !25 • Controller: • Doesn’t directly present another controllers • Tells that its finished via delegate or closure • Coordinator: • Listens to controller’s completion • Decides which screen to show next • Shares flow control with child coordinators
  20. P R E S E N TA B L E

    !26 protocol Presentable { var toPresent: UIViewController? { get } } extension UIViewController: Presentable { var toPresent: UIViewController? { return self } }
  21. !27 M O D U L E S T R

    U C T U R E
  22. !28 M O D U L E protocol ItemCreationModule: Presentable

    { typealias Completion = () -> Void var onFinish: Completion? { get set } } final class ItemCreationViewController: UIViewController, ItemCreationModule { var onFinish: Completion? }
  23. !30 protocol Animal { var name: String? { get }

    } class Cat: Animal { let name: String? init(name: String?) { self.name = name } }
  24. !31 let container = Container() container.register(Animal.self) { resolver in return

    Cat(name: "Mimi") } let animal = container.resolve(Animal.self)! print(animal.name) // "Mimi"
  25. !32 A S S E M B LY struct ItemCreationModuleAssembly:

    Assembly { func assemble(container: Container) { container.register(ItemCreationModule.self) { resolver in let controller = ItemCreationViewController() let viewModel = ItemCreationViewModelImpl() viewModel.itemsService = resolver.resolve(ItemsService.self) controller.viewModel = viewModel return controller } } }
  26. C O O R D I N AT O R

    protocol Coordinator: class { var router: Routable { get } func start() func start(with option: DeepLinkOption?) } !33
  27. R O U T E R protocol Routable: Presentable {

    func setRootModule(_ module: Presentable?, animated: Bool) func push(_ module: Presentable?, animated: Bool) func popModule(animated: Bool) func present(_ module: Presentable?, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) } !34
  28. D E E P L I N K I N

    G protocol DeepLinkOption { static func build(with userActivity: NSUserActivity) -> DeepLinkOption? static func build(with dict: [String: AnyObject]?) -> DeepLinkOption? } !35
  29. B A S E C O O R D I

    N AT O R !36 open class BaseCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] let router: Routable let assembler: Assembler init(assembler: Assembler, router: Routable) { self.assembler = assembler self.router = router } open func start(with option: DeepLinkOption?) { } }
  30. !37 extension BaseCoordinator { func addDependency(_ coordinator: Coordinator) { guard

    !childCoordinators.contains(where: { $0 === coordinator }) else { return } childCoordinators.append(coordinator) } func removeDependency(_ coordinator: Coordinator?) { guard let indexToRemove = childCoordinators.index(where: { $0 === coordinator }) else { return } childCoordinators.remove(at: indexToRemove) } func removeAllDependencies() { childCoordinators.removeAll() } }
  31. !38 I N I T class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow? private lazy var appCoordinator: Coordinator = makeAppCoordinator() func application(…didFinishLaunchingWithOptions…) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let notification = launchOptions?[.remoteNotification] as? [String: AnyObject] let deepLink = CEDeepLinkOption.build(with: notification) appCoordinator.start(with: deepLink) window?.makeKeyAndVisible() return true } }
  32. !39 I N I T class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow? private lazy var appCoordinator: Coordinator = makeAppCoordinator() func application(…didFinishLaunchingWithOptions…) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let notification = launchOptions?[.remoteNotification] as? [String: AnyObject] let deepLink = CEDeepLinkOption.build(with: notification) appCoordinator.start(with: deepLink) window?.makeKeyAndVisible() return true } }
  33. !40 I N I T class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow? private lazy var appCoordinator: Coordinator = makeAppCoordinator() func application(…didFinishLaunchingWithOptions…) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let notification = launchOptions?[.remoteNotification] as? [String: AnyObject] let deepLink = CEDeepLinkOption.build(with: notification) appCoordinator.start(with: deepLink) window?.makeKeyAndVisible() return true } }
  34. !41 extension AppDelegate { func makeAppCoordinator() -> Coordinator { let

    rootAssembler = Assembler( [ ServicesAssembly(), AppCoordinatorAssembly() ], container: Container() ) return rootAssembler.resolver.resolve( AppCoordinator.self, arguments: rootAssembler, window )! } }
  35. !42 extension AppDelegate { func makeAppCoordinator() -> Coordinator { let

    rootAssembler = Assembler( [ ServicesAssembly(), AppCoordinatorAssembly() ], container: Container() ) return rootAssembler.resolver.resolve( AppCoordinator.self, arguments: rootAssembler, window )! } }
  36. !43 extension AppDelegate { func makeAppCoordinator() -> Coordinator { let

    rootAssembler = Assembler( [ ServicesAssembly(), AppCoordinatorAssembly() ], container: Container() ) return rootAssembler.resolver.resolve( AppCoordinator.self, arguments: rootAssembler, window )! } }
  37. !44 extension AppDelegate { func makeAppCoordinator() -> Coordinator { let

    rootAssembler = Assembler( [ ServicesAssembly(), AppCoordinatorAssembly() ], container: Container() ) return rootAssembler.resolver.resolve( AppCoordinator.self, arguments: rootAssembler, window )! } }
  38. !45 extension AppDelegate { func makeAppCoordinator() -> Coordinator { let

    rootAssembler = Assembler( [ ServicesAssembly(), AppCoordinatorAssembly() ], container: Container() ) return rootAssembler.resolver.resolve( AppCoordinator.self, arguments: rootAssembler, window )! } }
  39. !46 extension AppDelegate { func makeAppCoordinator() -> Coordinator { let

    rootAssembler = Assembler( [ ServicesAssembly(), AppCoordinatorAssembly() ], container: Container() ) return rootAssembler.resolver.resolve( AppCoordinator.self, arguments: rootAssembler, window )! } }
  40. !47 final class AppCoordinatorImpl: BaseCoordinator, AppCoordinator { override func start(with

    option: DeepLinkOption?) { if authenticated { runMainFlow(with: option) } else { runAuthFlow(with: option) } } private func runAuthFlow(with deepLink: DeepLinkOption? = nil) { let coordinator = assembler.resolver.resolve(AuthCoordinator.self, argument: assembler)! coordinator.onFinish = { [weak self] deepLink in self?.removeDependency(coordinator) self?.runMainFlow(with: deepLink) } addDependency(coordinator) router.setRootModule(coordinator.router) coordinator.start(with: deepLink) } private func runMainFlow(with deepLink: DeepLinkOption?) { . . . } }
  41. !48 final class AppCoordinatorImpl: BaseCoordinator, AppCoordinator { override func start(with

    option: DeepLinkOption?) { if authenticated { runMainFlow(with: option) } else { runAuthFlow(with: option) } } private func runAuthFlow(with deepLink: DeepLinkOption? = nil) { let coordinator = assembler.resolver.resolve(AuthCoordinator.self, argument: assembler)! coordinator.onFinish = { [weak self] deepLink in self?.removeDependency(coordinator) self?.runMainFlow(with: deepLink) } addDependency(coordinator) router.setRootModule(coordinator.router) coordinator.start(with: deepLink) } private func runMainFlow(with deepLink: DeepLinkOption?) { . . . } }
  42. !49 final class AppCoordinatorImpl: BaseCoordinator, AppCoordinator { override func start(with

    option: DeepLinkOption?) { if authenticated { runMainFlow(with: option) } else { runAuthFlow(with: option) } } private func runAuthFlow(with deepLink: DeepLinkOption? = nil) { let coordinator = assembler.resolver.resolve(AuthCoordinator.self, argument: assembler)! coordinator.onFinish = { [weak self] deepLink in self?.removeDependency(coordinator) self?.runMainFlow(with: deepLink) } addDependency(coordinator) router.setRootModule(coordinator.router) coordinator.start(with: deepLink) } private func runMainFlow(with deepLink: DeepLinkOption?) { . . . } }
  43. !50 final class MainCoordinatorImpl: BaseCoordinator, MainCoordinator { . . .

    private func showItemsList() { var module = assembler.resolver.resolve(ItemsListModule.self) module?.onItemCreate = { [weak self] in self?.runItemCreationFlow(animated: true) } module?.onItemSelect = showDetails module?.onLogout = onLogout router.push(module, animated: false) } . . . }
  44. !51 struct MainCoordinatorAssembly: Assembly { func assemble(container: Container) { container.register(MainCoordinator.self)

    { (resolver, parentAssembler: Assembler) in let assembler = Assembler( [ ItemsListModuleAssembly(), ItemDetailsModuleAssembly(), ItemCreationCoordinatorAssembly() ], parent: parentAssembler ) let router = NavigationRouter(rootController: UINavigationController()) let coordinator = MainCoordinatorImpl(assembler: assembler, router: router) return coordinator } } }
  45. !52 struct MainCoordinatorAssembly: Assembly { func assemble(container: Container) { container.register(MainCoordinator.self)

    { (resolver, parentAssembler: Assembler) in let assembler = Assembler( [ ItemsListModuleAssembly(), ItemDetailsModuleAssembly(), ItemCreationCoordinatorAssembly() ], parent: parentAssembler ) let router = NavigationRouter(rootController: UINavigationController()) let coordinator = MainCoordinatorImpl(assembler: assembler, router: router) return coordinator } } }
  46. !53 struct MainCoordinatorAssembly: Assembly { func assemble(container: Container) { container.register(MainCoordinator.self)

    { (resolver, parentAssembler: Assembler) in let assembler = Assembler( [ ItemsListModuleAssembly(), ItemDetailsModuleAssembly(), ItemCreationCoordinatorAssembly() ], parent: parentAssembler ) let router = NavigationRouter(rootController: UINavigationController()) let coordinator = MainCoordinatorImpl(assembler: assembler, router: router) return coordinator } } }
  47. !54 struct MainCoordinatorAssembly: Assembly { func assemble(container: Container) { container.register(MainCoordinator.self)

    { (resolver, parentAssembler: Assembler) in let assembler = Assembler( [ ItemsListModuleAssembly(), ItemDetailsModuleAssembly(), ItemCreationCoordinatorAssembly() ], parent: parentAssembler ) let router = NavigationRouter(rootController: UINavigationController()) let coordinator = MainCoordinatorImpl(assembler: assembler, router: router) return coordinator } } }
  48. H O W T O C O N F I

    G U R E M O D U L E S I N N AV I G AT I O N ? !57 extension Presentable { func withRemovedBackItem() -> Presentable? { toPresent?.navigationItem.hidesBackButton = true return self } func withHiddenBottomBar() -> Presentable? { toPresent?.hidesBottomBarWhenPushed = true return self } }
  49. W H AT A B O U T D E

    E P L I N K I N G ? !58 private func runAuthFlow(with deepLink: DeepLinkOption? = nil) { let coordinator = [AuthCoordinator creation] coordinator.onFinish = { in […] runMainFlow(with: deepLink) } […] coordinator.start(with: deepLink) }
  50. B A C K B U T T O N

    B R E A K S T H E G A M E ? • We call dismiss directly • Back button and interactive pop work automatically • We don’t know when to dispose the coordinator • Make navigation router implement UINavigationControllerDelegate • Catch popped view controller and call its completion !59
  51. H O W T O M A K E I

    N I T I A L D E C I S I O N ? !60 final class LaunchInstructor { enum Instruction { case onboarding case auth case main } var instruction: Instruction { guard passedOnboarding else { return .onboarding } switch authStateProvider.authState { case false: return .auth case true: return .main } } }
  52. H O W T O PA S S T H

    E D ATA ? • Just pass data through methods • Store flow data in coordinator via dictionary • Store flow state in coordinator via storage object !61
  53. F L O W C O N T R O

    L L E R ? S O U N D S T H E S A M E ! • Flow controller is an UIViewController subclass • Flow controller adds dependencies as child view controllers • Coordinator is separated from UIKit == more flexible !62
  54. W O R K S W I T H V

    I P E R ? ! • Navigation logic is inside VIPER module • Maybe need to remove Router and listen to events from Presenter • VIPEC !63
  55. O K , I ’ M R E A D

    Y ! W H AT ’ S N E X T ? • Separate your navigation logic into flows • Start refactoring them one by one starting with root • Suffer • Live happily !64
  56. C O N C L U S I O N

    • We created reusable components – flows and modules • Modules don’t know about each other • They just do they work – receive input and return output • Now we can easily customise our flows !65
  57. U S E F U L L I N K

    S • Andrei Panov – Coordinators Essential • Soroush Khanlou – Presenting coordinator • Flow controller iOS • Onmyway133 – Coordinator and Flow controller • Swinject !67
  58. !68 private func runMainFlow(with deepLink: DeepLinkOption?) { let coordinator =

    coordinatorsFabric.makeMainCoordinator() coordinator.onLogout = { [weak self] in self?.removeDependency(coordinator) self?.runAuthFlow() } addDependency(coordinator) router.setRootModule(coordinator.router) coordinator.start(with: deepLink) } T H A N K S F O R Y O U R AT T E N T I O N a_rychkov A N Y E R R O R S ?