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

One Navigator to Rule them All

One Navigator to Rule them All

A technical talk on moving iOS navigation out of view controllers and into a separate class.

Sommer Panage

October 19, 2017
Tweet

More Decks by Sommer Panage

Other Decks in Technology

Transcript

  1. View Controller A probably has something like this: override func

    tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let detailData = tableData[indexPath.row] let detailVC = ViewControllerB() detailVC.data = detailData navigationController?.pushViewController(detailVC, animated: true) }
  2. View Controller B probably has something like this: init() {

    // Calls to super, etc. navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Go to C!", style: .plain, target: self, action: #selector(didTapGoButton)) } func didTapGoButton { let nextVC = ViewControllerC() navigationController?.pushViewController(nextVC, animated: true) }
  3. The omnipotent VC let detailVC = ViewControllerB() detailVC.data = detailData

    navigationController?.pushViewController(detailVC,animated: true) Our VC is acting like: • Parent • Child • Boss
  4. The Implicit Navigation Controller navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Go to C!",

    style: .plain, target: self, action: #selector(didTapGoButton)) If there's no nav bar, there's no "Go" button!
  5. View Controller A might have something like this: override func

    tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let detailData = tableData[indexPath.row] switch detailData.type { case .b: let bDetailVC = BViewController() bDetailVC.data = detailData navigationController?.pushViewController(bDetailVC, animated: true) case .d: let cDetailVC = DViewController() cDetailVC.data = detailData navigationController?.pushViewController(cDetailVC, animated: true) case .e: let eDetailVC = EViewController() eDetailVC.data = detailData navigationController?.pushViewController(eDetailVC, animated: true) } }
  6. What are the problems? • VCs are not independent •

    VCs depend on having parents that can do certain things • VCs are doing too much (flow, presentation, etc.)
  7. How does it work? • Each view controller that requires

    navigation has a delegate • Our root View Controller is MasterNavigationViewController: UINavigationController • MasterNavigationViewController configures every VC and is its delegate
  8. Our View Controllers protocol MovieListViewControllerDelegate: class { func didSelectFilm(with id:

    ID) } final class MovieListViewController: UIViewController { weak let delegate: MovieListViewControllerDelegate? init(delegate: MovieListViewControllerDelegate) { self.delegate = delegate super.init(nibName: nil, bundle: nil) } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) delegate.didSelectFilm(with: data[indexPath.row].id) } }
  9. Our View Controllers protocol MovieDetailViewControllerDelegate: class { func didSelectCharacter(with id:

    ID) } final class MovieDetailViewController: UIViewController { weak let delegate: MovieDetailViewControllerDelegate? init(delegate: MovieDetailViewControllerDelegate) { self.delegate = delegate super.init(nibName: nil, bundle: nil) } }
  10. Our View Controllers protocol CharacterViewControllerDelegate: class { func didSelectMovie(with id:

    ID) func didSelectStarship(with id: ID) } final class CharacterViewController: UIViewController { weak let delegate: CharacterViewControllerDelegate? init(delegate: CharacterViewControllerDelegate) { self.delegate = delegate super.init(nibName: nil, bundle: nil) } }
  11. The MasterNavigationViewController final class MasterNavigationViewController: UINavigationController { } extension MasterNavigationViewController:

    MovieListViewControllerDelegate { func didSelectFilm(with id: ID) { let detailVC = MovieDetailViewController(delegate: self) pushViewController(detailVC, animated: true) } }
  12. The MasterNavigationViewController extension MasterNavigationViewController: MovieDetailViewControllerDelegate { func didSelectCharacter(with id: ID)

    { /* Configure & Present CharacterVC */ } func didSelectPlanet(with id: ID) { /* Configure & Present PlanetVC */ } }
  13. What does this fix? • Individual VCs don't control flow/presentation

    • Individual VCs don't need to know about their parents • Individual VCs do not create other non-child VCs
  14. What isn't so great? • Doing all our magic in

    a view controller • Code duplication and/or implicit dependencies between delegates • Generally lots of boilerplate; everything has a navigation delegate!
  15. MasterNavigationViewController is still a view controller • As the app

    grows, this top level VC will likely house other logic control logic • Still tied to all the VC lifetime magic of UIKit • The VC delegate can easily get bloated with all kinds of tasks, not just nav
  16. Code duplication protocol MovieListViewControllerDelegate: class { func didSelectFilm(with id: ID)

    } protocol CharacterViewControllerDelegate: class { func didSelectMovie(with id: ID) }
  17. Code duplication extension MasterNavigationViewController: CharacterViewControllerDelegate { func didSelectMovie(with id: ID)

    { let detailVC = MovieDetailViewController(delegate: self) pushViewController(detailVC, animated: true) } } extension MasterNavigationViewController: MovieListViewControllerDelegate { func didSelectFilm(with id: ID) { let detailVC = MovieDetailViewController(delegate: self) pushViewController(detailVC, animated: true) } }
  18. Code duplication protocol CharacterViewControllerDelegate: class { func didSelectFilm(with id: ID)

    } extension MasterNavigationViewController: CharacterViewControllerDelegate { // didSelectFilm handled by MovieListViewControllerDelegate implementation } extension MasterNavigationViewController: MovieListViewControllerDelegate { func didSelectFilm(with id: ID) { let detailVC = MovieDetailViewController(delegate: self) pushViewController(detailVC, animated: true) } }
  19. Code duplication protocol CharacterViewControllerDelegate: class { func didSelectFilm(with id: ID)

    } extension MasterNavigationViewController: CharacterViewControllerDelegate { // didSelectFilm handled by MovieListViewControllerDelegate implementation } extension MasterNavigationViewController: MovieListViewControllerDelegate { func didSelectFilm(with id: ID) { let crawlVC = MovieCrawlViewController(delegate: self) pushViewController(crawlVC, animated: true) } }
  20. So much boilerplate • Every view controller that can navigate

    must now have a delegate. • Boilerplate grows as a function of how much navigation is possible.
  21. What's so great about the Navigator? • A standalone object

    • Not tied to View Controller life cycle or responsibilites • We can use Navigator Protocols to not have to repeat boilerplate... • ...and to group functionality
  22. How does it work? • Navigator is a base protocol

    • All navigator protocols inherit form Navigator • Navigator protocols are grouped by functionality, not per VC
  23. Protocols are functionality based, not VC based protocol Navigator {

    // Any navigation functionalities we want available to all VCs can live here } protocol MovieNavigator: Navigator { func viewController(_ viewController: UIViewController, didSelectMovie id: ID) } protocol CharacterNavigator: Navigator { func viewController(_ viewController: UIViewController, didSelectCharacter id: ID) } protocol StarshipNavigator: Navigator { func viewController(_ viewController: UIViewController, didSelectStarship id: ID) }
  24. Each VC declares which functionality it needs by selecting navigators

    final class MoviesListViewController: UIViewController { typealias MyNavigator = MovieNavigator let navigator: MyNavigator init(navigator: MyNavigator) { self.navigator = navigator super.init(nibName: nil, bundle: nil) } } final class CharacterViewController: UIViewController { typealias MyNavigator = MovieNavigator & StarshipNavigator let navigator: MyNavigator init(navigator: MyNavigator) { self.navigator = navigator super.init(nibName: nil, bundle: nil) } }
  25. Adding functionality is as simple as requiring another navigator final

    class CharacterViewController: UIViewController { typealias MyNavigator = MovieNavigator & StarshipNavigator let navigator: MyNavigator init(navigator: MyNavigator) { self.navigator = navigator super.init(nibName: nil, bundle: nil) } }
  26. Adding functionality is as simple as requiring another navigator final

    class CharacterViewController: UIViewController { typealias MyNavigator = MovieNavigator & StarshipNavigator & ErrorNavigator let navigator: MyNavigator init(navigator: MyNavigator) { self.navigator = navigator super.init(nibName: nil, bundle: nil) } }
  27. Implementing the AppNavigator • The AppNavigator implements all Navigator protocols

    in extensions • The AppDelegate creates and runs the AppNavigator • AppNavigator sets up your rootVC and keeps a reference to it • AppNavigator has one job; manage app wide navigation • AppNavigator is not a view controller, just a class
  28. The AppNavigator takes control navigation final class AppNavigator: Navigator {

    fileprivate let navigationController: UINavigationController init(navigationController: UINavigationController) { self.navigationController = navigationController // configuration if navigationController goes here } func run() { let listVC = MoviesListViewController(navigator: self) navigationController.setViewControllers([listVC], animated: false) } }
  29. The AppNavigator implements all Navigator protocols extension AppNavigator: MovieNavigator {

    func viewController(_ viewController: UIViewController, didSelectMovie id: ID) { let movieVC = MovieDetailViewController(navigator: self) navigationController.pushViewController(movieVC, animated: true) } } extension AppNavigator: CharacterNavigator { func viewController(_ viewController: UIViewController, didSelectCharacter id: ID) { let characterVC = CharacterViewController(navigator: self) navigationController.pushViewController(characterVC, animated: true) } } extension AppNavigator: StarshipNavigator { func viewController(_ viewController: UIViewController, didSelectStarship id: ID) { let shipVC = StarshipViewController(navigator: self) navigationController.pushViewController(shipVC, animated: true) } }
  30. We can use the viewController param to differentiate different flows

    when needed extension AppNavigator: MovieNavigator { func viewController(_ viewController: UIViewController, didSelectMovie id: ID) { let movieVC = MovieDetailViewController(navigator: self) // Decomp this out of course if viewController is CharacterViewController { movieVC.navigationItem.rightBarButtonItem = UIBarButtonItem( title: "...", style: .plain, target: self, action: #selector(didTapMore)) } navigationController.pushViewController(movieVC, animated: true) } }
  31. Did we solve our problems? • By defining our navigator

    protocols by functionality rather than by VC, we've avoided lots of boilerplate... • ...and code duplication. • We are no longer coupled to the VC lifecycle.
  32. Benefits beyond our iPhone app right now • Apple TV?

    iPad? • "The big rewrite" • Move fast and don't break things
  33. Adding complexity • Top level AppNavigator can have sub-navigators. •

    Each layer of navigation can be managed by its own navigator.
  34. A stack of subnavigators @objc private func didTapMore() { let

    crawlNavVC = UINavigationController() let newNavigator = CrawlNavigator(crawlNavVC, api: api) { [unowned self] in crawlNavVC.dismiss(animated: true, completion: nil) self.removeNavigator(crawlNav) } addNavigator(newNavigator) newNavigator.run() navigationController.present(navVC, animated: true, completion: nil) }
  35. Hat tip to the Coordinator Pattern • Coordinator handles navigation

    and model mutation • Delegation is per VC
  36. Summary • View Controllers should be reusable and reorderable easily

    • View Controllers should avoid any assumptions about where they live in the app • Navigators capture the "flow" of these individual entities • Navigators allow us to separate the what from the how
  37. Thank you! Contact me: @Sommer on Twitter Thanks to... •

    www.swapi.com (i.e. the Star Wars API) • iOS Conf SG