|
| 1 | +# Навигация |
| 2 | + |
| 3 | +В основе навигации лежат координаторы. Каждый координатор покрывает логически связанный блок |
| 4 | +функционала, который чаще всего состоит из нескольких экранов. При этом между собой они независимы и |
| 5 | +отвечают только за цепочку переходов внутри себя. Также имеют возможность получать настройку |
| 6 | +действия, которое должно быть выполнено после завершения блока ответственности координатора. |
| 7 | + |
| 8 | +## Пример |
| 9 | + |
| 10 | +Предположим, что у нас есть приложение с авторизацией и списком новостей, с которого |
| 11 | +можно перейти к детальному просмотру каждой новости и в раздел настроек для конфигурации отображения новостей. |
| 12 | + |
| 13 | +Это разобьётся на 4 координатора: |
| 14 | + |
| 15 | +```mermaid |
| 16 | +graph TD |
| 17 | + AppCoordinator --> AuthCoordinator |
| 18 | + AppCoordinator --> NewsCoordinator |
| 19 | + NewsCoordinator --> SettingsCoordinator |
| 20 | +``` |
| 21 | + |
| 22 | +- AppCoordinator |
| 23 | + - Стартовый координатор. Всегда является первой входной точкой, определяет, куда должен выполниться дальнейший переход при запуске приложения |
| 24 | + - Если юзер не авторизован - запустит координатор авторизации и в качестве completionHandler-а укажет ему переход на список новостей в случае успешной авторизации |
| 25 | + - Если юзер уже авторизован - запустит координатор просмотра списка новостей |
| 26 | +- AuthCoordinator |
| 27 | + - Запустит процесс авторизации |
| 28 | + - Будет совершать переходы по всем требуемым шагам - например ввод логина/пароля, смс-кода, установки никнейма и т.п. |
| 29 | + - По итогу успешной авторизации вызовет переданный ему на вход completionHandler. |
| 30 | +- NewsCoordinator |
| 31 | + - Отвечает за показ списка новостей |
| 32 | + - Реализовывает переход в детали конкретной новости внутри этого же координатора |
| 33 | + - При переходе в настройки создаёт координатор настроек, в качестве completionHandler-а может передать ему логику обновления своего списка новостей. Если в настройках изменились параметры - обновляет список |
| 34 | +- SettingsCoordinator |
| 35 | + - Отвечает за работу с экраном настроек |
| 36 | + - При завершении работы и применении настроек вызывает completion, чтобы новости обновились |
| 37 | + |
| 38 | +# BaseCoordinator |
| 39 | + |
| 40 | +Чтобы работать с координаторами было проще, используется базовый класс, от которого наследуются |
| 41 | +остальные. В директории `Common/Coordinator` вы найдете файлы `CoordinatorProtocol.swift` и `BaseCoordinator.swift`. Первый несет в себе протокол, под который подписан `BaseCoordinator` и описывает обязательные методы и поля: |
| 42 | + |
| 43 | +```swift |
| 44 | +protocol Coordinator: AnyObject { |
| 45 | + var completionHandler: (()->())? { get } |
| 46 | + func start() |
| 47 | + func clear() |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +По сути он должен иметь ровно три вещи - completionHandler, который вызовется при завершении его логической зоны ответственности. Функцию start, при вызове которой он начинает запускать свой флоу таким образом, каким считает нужным, и функцию clear, которая чистит сам координатор и все дочерние. |
| 52 | + |
| 53 | +Ну а второй несет сам класс базового координатора, который реализует этот протокол: |
| 54 | + |
| 55 | +```swift |
| 56 | +class BaseCoordinator: NSObject, Coordinator, UINavigationControllerDelegate { |
| 57 | + var childCoordinators: [Coordinator] = [] |
| 58 | + var completionHandler: (() -> ())? |
| 59 | + |
| 60 | + let window: UIWindow |
| 61 | + let factory: SharedFactory |
| 62 | + |
| 63 | + var navigationController: UINavigationController? |
| 64 | + |
| 65 | + init(window: UIWindow, factory: SharedFactory) { ... } |
| 66 | + |
| 67 | + func addDependency<Child>(_ coordinator: Child, completion: (() -> Void)? = nil) -> Child where Child : BaseCoordinator { ... } |
| 68 | + |
| 69 | + func clear() { ... } |
| 70 | + |
| 71 | + //Cases |
| 72 | + //1. Initial with window - create NV, etc.. |
| 73 | + //2. Exists navcontroller, |
| 74 | + |
| 75 | + func start() { |
| 76 | + // |
| 77 | + } |
| 78 | + |
| 79 | + func beginInNewNavigation(_ controller: UIViewController) -> UINavigationController { ... } |
| 80 | + |
| 81 | + func beginInExistNavigation(_ controller: UIViewController) { ... } |
| 82 | + |
| 83 | + func currentViewController() -> UIViewController { ... } |
| 84 | +} |
| 85 | + |
| 86 | +``` |
| 87 | + |
| 88 | +Для инициализации необходим window и factory. Также можно указать NavigationController с предыдущего |
| 89 | +координатора, для сохранения общей навигации. |
| 90 | + |
| 91 | +:::note |
| 92 | + |
| 93 | +Координаторам нужен factory для доступа к фабрикам фичей из общей библиотеки. |
| 94 | + |
| 95 | +::: |
| 96 | + |
| 97 | +Добавление и удаление зависимостей нужны для корректной очистки связей и памяти при построении |
| 98 | +цепочек координаторов. |
| 99 | + |
| 100 | +Также есть вспомогательные методы, которые позволяют получить текущий контроллер - |
| 101 | +currentViewController и совершить переход назад - popBack. |
| 102 | + |
| 103 | +:::caution |
| 104 | +От проекта к проекту базовый координатор может изменяться, обеспечивая дополнительные нужды проекта. |
| 105 | +::: |
| 106 | + |
| 107 | +## AppСoordinator |
| 108 | + |
| 109 | +Теперь когда мы поняли принцип работы координаторов, посмотрим на класс `AppCoordinator`: |
| 110 | + |
| 111 | +```swift |
| 112 | +class AppCoordinator: BaseCoordinator { |
| 113 | + override func start() { |
| 114 | + let vc = UIViewController() |
| 115 | + vc.view.backgroundColor = .green |
| 116 | + self.window.rootViewController = vc |
| 117 | + } |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +В данном случае, главный координатор совсем простой - создает контроллер зелёного цвета и делает его главным экраном window. |
| 122 | + |
| 123 | +Теперь посмотрим где происходит создание главного координатора. Идём в `AppDelegate.swift`: |
| 124 | + |
| 125 | +```swift |
| 126 | + // .... |
| 127 | + |
| 128 | + // переменная координатора |
| 129 | + private (set) var coordinator: AppCoordinator! |
| 130 | + |
| 131 | + func application(_ application: UIApplication, |
| 132 | + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { |
| 133 | + |
| 134 | + //... |
| 135 | + |
| 136 | + // его инициализация |
| 137 | + coordinator = AppCoordinator.init( |
| 138 | + window: self.window!, |
| 139 | + factory: AppComponent.factory |
| 140 | + ) |
| 141 | + // запуск координатора |
| 142 | + coordinator.start() |
| 143 | + |
| 144 | + // .... |
| 145 | + } |
| 146 | +``` |
| 147 | + |
| 148 | +Теперь дальнейшая логика переходов зависит от текущего контроллера и действий юзера на нём. |
| 149 | + |
| 150 | +После данного разбора у вас должно сформироваться представление о том, какие подходы мы используем |
| 151 | +для реализации навигации в iOS-приложении. |
| 152 | + |
| 153 | +## Материалы |
| 154 | + |
| 155 | +- [Статья - How to use the coordinator pattern in iOS apps](https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps) |
| 156 | +- [Статья - Coordinator Tutorial for iOS: Getting Started](https://www.raywenderlich.com/158-coordinator-tutorial-for-ios-getting-started) |
| 157 | +- [Видео-разбор](https://www.youtube.com/watch?v=Pt9TGFzLVzc) использования `ApplicationCoordinator` для навигации между экранами |
0 commit comments