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 решает, показать онбординг, логин или главный экран — в зависимости от состояния приложения. 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 | Типобезопасно, явно, раздельные протоколы | Много протоколов, много бойлерплейта |
| Closure | Компактно, меньше файлов | Трудно отлаживать retain cycle |
| Combine/Rx | Реактивный, легко комбинировать | Зависимость от библиотеки, сложнее Debug |
Shared data layer — Coordinator'ы не передают данные напрямую, а используют общий сервис/репозиторий. AuthCoordinator сохраняет токен в Keychain/UserDefaults, ProfileCoordinator читает оттуда же. Coordinator'ы общаются через общее состояние (Dependency Injection container), а не через прямые вызовы. Это снижает связанность 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 вызывает coordinator через протокол. MVVM-C — стандартный подход для iOS-проектов с SwiftUI: Coordinator управляет NavigationStack или fullScreenCover, ViewModel вызывает coordinator публикацией состояния. 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 navigation. Coordinator имеет смысл для сложных flow с глубокими условиями (онбординг-логин-главный экран в зависимости от роли). Для простых приложений на 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 (онбординг с ветвлениями, авторизация с восстановлением пароля) и mixed UIKit/SwiftUI проектами.
Mock Router — проверка, какие методы вызываются и с какими параметрами. Проверка childCoordinators: после start() массив не пустой, после finish() — пустой. Coordinator тестируется без UI: Router — протокол, его mock не требует UIKit. Используйте XCTestExpectation для async flow. В Android аналоги — тестирование NavigationController и NavHost с mock-навигацией.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также