Skip to main content

Навигация

В основе навигации лежат координаторы. Каждый координатор покрывает логически связанный блок функционала, который чаще всего состоит из нескольких экранов. При этом между собой они независимы и отвечают только за цепочку переходов внутри себя. Также имеют возможность получать настройку действия, которое должно быть выполнено после завершения блока ответственности координатора.

Пример

Предположим, что у нас есть приложение с авторизацией и списком новостей, с которого можно перейти к детальному просмотру каждой новости и в раздел настроек для конфигурации отображения новостей.

Это разобьётся на 4 координатора:

graph TD AppCoordinator --> AuthCoordinator AppCoordinator --> NewsCoordinator NewsCoordinator --> SettingsCoordinator
  • AppCoordinator
    • Стартовый координатор. Всегда является первой входной точкой, определяет, куда должен выполниться дальнейший переход при запуске приложения
    • Если юзер не авторизован - запустит координатор авторизации и в качестве completionHandler-а укажет ему переход на список новостей в случае успешной авторизации
    • Если юзер уже авторизован - запустит координатор просмотра списка новостей
  • AuthCoordinator
    • Запустит процесс авторизации
    • Будет совершать переходы по всем требуемым шагам - например ввод логина/пароля, смс-кода, установки никнейма и т.п.
    • По итогу успешной авторизации вызовет переданный ему на вход completionHandler.
  • NewsCoordinator
    • Отвечает за показ списка новостей
    • Реализовывает переход в детали конкретной новости внутри этого же координатора
    • При переходе в настройки создаёт координатор настроек, в качестве completionHandler-а может передать ему логику обновления своего списка новостей. Если в настройках изменились параметры - обновляет список
  • SettingsCoordinator
    • Отвечает за работу с экраном настроек
    • При завершении работы и применении настроек вызывает completion, чтобы новости обновились

BaseCoordinator

Чтобы работать с координаторами было проще, используется базовый класс, от которого наследуются остальные. В директории Common/Coordinator вы найдете файлы CoordinatorProtocol.swift и BaseCoordinator.swift. Первый несет в себе протокол, под который подписан BaseCoordinator и описывает обязательные методы и поля:

protocol Coordinator: AnyObject {
var completionHandler: (()->())? { get }
func start()
func clear()
}

По сути он должен иметь ровно три вещи - completionHandler, который вызовется при завершении его логической зоны ответственности. Функцию start, при вызове которой он начинает запускать свой флоу таким образом, каким считает нужным, и функцию clear, которая чистит сам координатор и все дочерние.

Ну а второй несет сам класс базового координатора, который реализует этот протокол:

class BaseCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
var childCoordinators: [Coordinator] = []
var completionHandler: (() -> ())?

let window: UIWindow
let factory: SharedFactory

var navigationController: UINavigationController?

init(window: UIWindow, factory: SharedFactory) { ... }

func addDependency<Child>(_ coordinator: Child, completion: (() -> Void)? = nil) -> Child where Child : BaseCoordinator { ... }

func clear() { ... }

//Cases
//1. Initial with window - create NV, etc..
//2. Exists navcontroller,

func start() {
//
}

func beginInNewNavigation(_ controller: UIViewController) -> UINavigationController { ... }

func beginInExistNavigation(_ controller: UIViewController) { ... }

func currentViewController() -> UIViewController { ... }
}

Для инициализации необходим window и factory. Также можно указать NavigationController с предыдущего координатора, для сохранения общей навигации.

note

Координаторам нужен factory для доступа к фабрикам фичей из общей библиотеки.

Добавление и удаление зависимостей нужны для корректной очистки связей и памяти при построении цепочек координаторов.

Также есть вспомогательные методы, которые позволяют получить текущий контроллер - currentViewController и совершить переход назад - popBack.

caution

От проекта к проекту базовый координатор может изменяться, обеспечивая дополнительные нужды проекта.

AppСoordinator

Теперь когда мы поняли принцип работы координаторов, посмотрим на класс AppCoordinator:

class AppCoordinator: BaseCoordinator {
override func start() {
let vc = UIViewController()
vc.view.backgroundColor = .green
self.window.rootViewController = vc
}
}

В данном случае, главный координатор совсем простой - создает контроллер зелёного цвета и делает его главным экраном window.

Теперь посмотрим где происходит создание главного координатора. Идём в AppDelegate.swift:

    // ....

// переменная координатора
private (set) var coordinator: AppCoordinator!

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {

//...

// его инициализация
coordinator = AppCoordinator.init(
window: self.window!,
factory: AppComponent.factory
)
// запуск координатора
coordinator.start()

// ....
}

Теперь дальнейшая логика переходов зависит от текущего контроллера и действий юзера на нём.

После данного разбора у вас должно сформироваться представление о том, какие подходы мы используем для реализации навигации в iOS-приложении.

Материалы