Coordinator (координатор) — архитектурен навигационен модел, който изнася логиката на преходите между екрани от ViewController в отделни класове. Моделът е предложен от Soroush Khanlou през 2015 г. и получава широко разпространение в iOS общността. Coordinator управлява flow-а на приложението: създава и показва ViewController, предава данни между екрани и обработва завършването на flow-а. Моделът решава проблема Massive View Controller, изнасяйки навигацията от контролера. Повече — в оригиналната статия за Coordinator.
Основни точки
Coordinator — модел, който поема отговорността за навигацията в iOS приложение. В стандартния UIKit ViewController сам управлява преходите: present, push, show segue — всички навигационни методи се извикват от UIViewController. Coordinator изнася тази логика: ViewController съобщава за събитие (напр. „потребителят натисна бутона за вход“), Coordinator решава кой екран да се покаже следващ. ViewController остава само с UI логиката и делегира навигацията на координатора.
Структура на модела — CoordinatorProtocol с методи start() и finish(). start() — начало на flow-а: създаване на първия ViewController и показване. finish() — завършване на flow-а с уведомяване на родителския координатор. Router — обвивка около UINavigationController (или UISplitViewController), предоставяща методите show, push, pop, dismiss. Координаторът не работи директно с UINavigationController — само чрез Router. Това позволява тестване на навигацията и смяна на UI рамката.
| Компонент | Роля | Пример |
|---|---|---|
| Coordinator | Управление на навигационния flow | AuthCoordinator, ProfileCoordinator |
| Router | Абстракция над UINavigationController | push, present, pop, dismiss |
| ViewController | UI + делегиране на събития на Coordinator | LoginViewController.delegate |
Проблеми, които Coordinator решава — Massive View Controller (навигацията — честа причина за разрастване на контролера). В стандартния UIKit ViewController съдържа prepareForSegue, навигационни делегати, обработка на unwind segues. Coordinator елиминира това. Segue в storyboard — статична връзка между екрани, Coordinator предоставя динамична навигация с условия. Тестването на навигация става възможно: Coordinator може да се тества без UI чрез проверка на последователността от извиквания на Router.
Базов Coordinator в Swift — протокол с асоцииран тип за Router и методи start/finish. Router — протокол, абстрахиращ UINavigationController. Конкретната имплементация на Router обвива UINavigationController и делегира методите му. Coordinator приема Router в init и го използва за навигация. Дъщерните координатори се съхраняват в масива childCoordinators за управление на жизнения цикл.
// Router — абстракция на навигация
protocol RouterProtocol: AnyObject {
func push(_ viewController: UIViewController, animated: Bool)
func pop(animated: Bool)
func present(_ viewController: UIViewController, animated: Bool)
func dismiss(animated: Bool)
}
final class NavigationRouter: RouterProtocol {
private let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func push(_ vc: UIViewController, animated: Bool) {
navigationController.pushViewController(vc, animated: animated)
}
func pop(animated: Bool) {
navigationController.popViewController(animated: animated)
}
func present(_ vc: UIViewController, animated: Bool) {
navigationController.present(vc, animated: animated)
}
func dismiss(animated: Bool) {
navigationController.dismiss(animated: animated)
}
}
// Coordinator — управление на flow
protocol CoordinatorProtocol: AnyObject {
var childCoordinators: [CoordinatorProtocol] { get set }
var router: RouterProtocol { get }
func start()
func finish()
}
class AuthCoordinator: CoordinatorProtocol {
var childCoordinators: [CoordinatorProtocol] = []
let router: RouterProtocol
init(router: RouterProtocol) {
self.router = router
}
func start() {
let loginVC = LoginViewController()
loginVC.onLogin = { [weak self] in
self?.showHome()
}
router.push(loginVC, animated: true)
}
private func showHome() {
let homeCoordinator = HomeCoordinator(router: router)
childCoordinators.append(homeCoordinator)
homeCoordinator.start()
}
func finish() {
childCoordinators.removeAll()
router.pop(animated: true)
}
}
Създаване на Coordinator в AppDelegate/SceneDelegate — AppDelegate или SceneDelegate създава UINavigationController, обвива го в NavigationRouter, създава коренов Coordinator (AppCoordinator) и извиква start(). AppCoordinator решава да покаже onboarding, вход или главен екран — в зависимост от състоянието на приложението. Coordinator — единствената входна точка за навигация, ViewController не знае за други екрани.
Йерархия на координаторите — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Дъщерният координатор се създава от родителя и се съхранява в масива childCoordinators. Когато дъщерният координатор завърши работата си, той извиква finish() при родителя и родителят го премахва от childCoordinators. Това предотвратява изтичане на памет: Coordinator има силна референция към ViewController (чрез Router) и без премахване от childCoordinators обектът няма да бъде освободен.
// Делегат за комуникация Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — родител
class AppCoordinator: AuthCoordinatorDelegate {
func startAuthFlow() {
let authCoordinator = AuthCoordinator(router: router)
authCoordinator.delegate = self
childCoordinators.append(authCoordinator)
authCoordinator.start()
}
func authCoordinatorFinished(_ coordinator: AuthCoordinator) {
childCoordinators.removeAll { $0 is AuthCoordinator }
startMainFlow()
}
}
Управление на childCoordinators — премахването на Coordinator от масива е единственият начин да се освободи. Ако забравите да премахнете завършилия Coordinator, той остава в паметта заедно с ViewController-ите. Препоръчва се: didMove(toParent:) на родителя, callback при завършване или Combine publisher за автоматично премахване. Моделът Coordinator не специфицира механизма за уведомяване — делегат, closure или Combine — изборът е на разработчика.
Предаване на данни чрез делегат — дъщерният Coordinator дефинира протокол на делегат с методи, чрез които се предават резултатите: func authCoordinator(_:didLoginWith user: User). Родителят имплементира протокола и получава данни при завършване на дъщерния flow. Това е типобезопасно и изрично. Недостатък: за всеки дъщерен Coordinator трябва да се напише отделен протокол. За проекти с 10+ Coordinator това води до увеличаване на броя файлове.
Предаване на данни чрез тип Result — методът finish приема Result
| Метод | Предимства | Недостатъци |
|---|---|---|
| Delegate | Типобезопасно, изрично, отделни протоколи | Много протоколи, много boilerplate |
| Closure | Компактно, по-малко файлове | Трудно за дебъгване на retain cycle |
| Combine/Rx | Реактивно, лесно за комбиниране | Зависимост от библиотека, по-труден Debug |
Споделен слой от данни — Coordinator-ите не предават данни директно, а използват обща услуга/репозиторий. AuthCoordinator записва token в Keychain/UserDefaults, ProfileCoordinator чете оттам. Coordinator-ите комуникират чрез споделено състояние (Dependency Injection контейнер), а не чрез директни извиквания. Това намалява свързаността на Coordinator-ите, но създава имплицитни зависимости от споделеното състояние.
Coordinator vs Router — Router е компонент на Coordinator, който абстрахира UINavigationController. Coordinator отговаря за flow-а (кой екран да се покаже), Router — за механиката (как да се покаже: push/present). Router е „как“, Coordinator е „какво“. Router може да се използва без Coordinator (напр. Navigator-сингълтън), но Coordinator без Router — просто ViewController с различна абстракция. Обикновено двата модела се използват заедно.
Coordinator vs VIPER — VIPER има компонент Wireframe, отговорен за навигация — аналог на Coordinator. В VIPER Wireframe е част от модула, Coordinator — отделен слой над модулите. VIPER модулът (View-Interactor-Presenter-Entity-Router) включва навигацията като част от модула. Coordinator е външен спрямо модулите: създава и свързва модули, но не е част от техния състав. Coordinator е по-гъвкав за повторно използване на екрани в различни flow-ове.
MVVM-C — разширение на MVVM с Coordinator. ViewModel не познава директно Coordinator — ViewController делегира навигацията чрез ViewModel, ViewModel извиква координатора чрез протокол. MVVM-C — стандартният подход за iOS проекти със SwiftUI: Coordinator управлява NavigationStack или fullScreenCover, ViewModel извиква координатора чрез публикуване на състояние. Apple не препоръчва Coordinator за SwiftUI — NavigationStack и NavigationPath са вградени механизми за навигация.
// MVVM-C: ViewModel извиква Coordinator чрез протокол
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// логика...
navigation?.showMainScreen()
}
}
Често задавани въпроси
За SwiftUI вградената навигация (NavigationStack, NavigationPath) често замества Coordinator. Apple препоръчва path-based навигация. Coordinator има смисъл за сложни flow с дълбоки условия (onboarding-вход-главен екран в зависимост от ролята). За прости SwiftUI приложения Coordinator е излишен — използвайте NavigationPath.
Не, това са различни модели. Coordinator управлява навигационния flow: решава кой екран да се покаже, създава ViewController и ги свързва. Router — абстракция над UINavigationController: push, present, pop, dismiss. Coordinator използва Router за изпълнение на навигация. В някои имплементации Router включва логиката на Coordinator (Router-per-screen), но това е отклонение от оригиналния модел.
Две основни места за изтичане: childCoordinators (родителят държи дъщерния, забравяйки да го премахне) и Router (UINavigationController държи ViewController). Решение: винаги премахвайте дъщерния Coordinator от масива при finish(). Използвайте weak референция за delegate. За Router — не дръжте силна референция към UINavigationController, ако вече е в йерархията на window. Тествайте deinit на Coordinator.
За приложения с 3-5 екрана Coordinator е излишен — segue или прост navigationController.pushViewController са по-прости. За SwiftUI приложения с NavigationStack — също излишен. Coordinator е оправдан за приложения с 15+ екрана, сложни flow (onboarding с разклонения, авторизация с възстановяване на парола) и смесени UIKit/SwiftUI проекти.
Mock Router — проверка кои методи се извикват и с какви параметри. Проверка на childCoordinators: след start() масивът не е празен, след finish() — празен. Coordinator се тества без UI: Router е протокол, неговият mock не изисква UIKit. Използвайте XCTestExpectation за асинхронен flow. В Android аналог — тестване на NavigationController и NavHost с mock-навигация.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също