Coordinator: ключевые понятия, паттерн координатор для навигации iOS

Автор: IT Sectr Опубликовано: 2026-02-18 Время чтения: 9 мин

Coordinator (координатор) — архитектурный паттерн навигации, который выносит логику переходов между экранами из ViewController в отдельные классы. Паттерн предложен Soroush Khanlou в 2015 году и получил широкое распространение в iOS-сообществе. Coordinator управляет flow приложения: создаёт и отображает ViewController, передаёт данные между экранами и обрабатывает завершение flow. Паттерн решает проблему Massive View Controller, вынося навигацию из контроллера. Подробнее — в оригинальной статье Coordinator.

Главное

  • Coordinator — паттерн навигации, выносящий логику переходов из ViewController
  • Разделение ответственности — ViewController управляет UI, Coordinator — навигацией
  • Router — вспомогательный компонент Coordinator для абстракции UINavigationController
  • Flow — последовательность экранов, управляемая одним Coordinator (например, onboarding)
  • Делегирование — дочерние координаторы отчитываются родителю через delegate/protocol

Что такое 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Абстракция над UINavigationControllerpush, present, pop, dismiss
ViewControllerUI + делегирование событий CoordinatorLoginViewController.delegate

Проблемы, которые решает Coordinator — Massive View Controller (навигация — частая причина разрастания контроллера). В стандартном UIKit ViewController содержит prepareForSegue, делегаты навигации, обработку unwind segues. Coordinator устраняет это. Segue в storyboard — статическая связь между экранами, Coordinator даёт динамическую навигацию с условиями. Тестирование навигации становится возможным: можно протестировать Coordinator без UI, проверив последовательность вызовов Router.

Coordinator на Swift: реализация с Router и Flow

Базовый Coordinator на Swift — протокол с ассоциированным типом для Router и методами start/finish. Router — протокол, абстрагирующий UINavigationController. Конкретная реализация Router оборачивает UINavigationController и делегирует ему методы. Coordinator принимает Router в init и использует его для навигации. Дочерние координаторы хранятся в массиве childCoordinators для управления жизненным циклом.

swift
// 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 не знает о других экранах.

Дочерние Coordinator и иерархия координаторов

Иерархия координаторов — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Дочерний координатор создаётся родителем и хранится в массиве childCoordinators. Когда дочерний координатор завершает работу, он вызывает finish() у родителя, и родитель удаляет его из childCoordinators. Это предотвращает утечки памяти: Coordinator имеет сильную ссылку на ViewController (через Router), и без удаления из childCoordinators объект не освободится.

swift
// Делегат для связи 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, где Output — generic-тип результата flow. Coordinator — дженерик с ассоциированным типом результата. start() с callback: start(completion: @escaping (Output) -> Void). Это сокращает код: не нужно писать отдельный протокол для каждого coordinator. RxSwift/Combine: Coordinator публикует результат через PassthroughSubject/Publisher. Выбор зависит от архитектурного подхода команды.

МетодПлюсыМинусы
DelegateТипобезопасно, явно, раздельные протоколыМного протоколов, много бойлерплейта
ClosureКомпактно, меньше файловТрудно отлаживать retain cycle
Combine/RxРеактивный, легко комбинироватьЗависимость от библиотеки, сложнее Debug

Shared data layer — Coordinator'ы не передают данные напрямую, а используют общий сервис/репозиторий. AuthCoordinator сохраняет токен в Keychain/UserDefaults, ProfileCoordinator читает оттуда же. Coordinator'ы общаются через общее состояние (Dependency Injection container), а не через прямые вызовы. Это снижает связанность Coordinator'ов, но создаёт неявные зависимости от общего состояния.

Сравнение Coordinator с Router, VIPER и MVVM-C

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 встроенные механизмы навигации.

swift
// MVVM-C: ViewModel вызывает Coordinator через протокол
protocol AuthNavigationProtocol: AnyObject {
    func showMainScreen()
    func showForgotPassword()
}

class AuthViewModel: ObservableObject {
    weak var navigation: AuthNavigationProtocol?

    func loginTapped() {
        // логика...
        navigation?.showMainScreen()
    }
}

Часто задаваемые вопросы

Нужен ли Coordinator для SwiftUI?

Для SwiftUI встроенная навигация (NavigationStack, NavigationPath) часто заменяет Coordinator. Apple рекомендует path-based navigation. Coordinator имеет смысл для сложных flow с глубокими условиями (онбординг-логин-главный экран в зависимости от роли). Для простых приложений на SwiftUI Coordinator избыточен — используйте NavigationPath.

Coordinator — это Router?

Нет, это разные паттерны. Coordinator управляет flow навигации: решает, какой экран показать, создаёт ViewController и связывает их. Router — абстракция над UINavigationController: push, present, pop, dismiss. Coordinator использует Router для выполнения навигации. В некоторых реализациях Router включает логику Coordinator (Router-per-screen), но это отклонение от оригинального паттерна.

Как избежать retain cycle в Coordinator?

Два основных места утечек: childCoordinators (родитель держит дочернего, забыв удалить) и Router (UINavigationController держит ViewController). Решение: всегда удалять дочерний Coordinator из массива при finish(). Использовать weak ссылку для delegate. Для Router — не держать сильную ссылку на UINavigationController, если он уже в иерархии window. Тестируйте deinit Coordinator'а.

Когда Coordinator — избыточен?

Для приложений с 3-5 экранами Coordinator избыточен — segue или простой navigationController.pushViewController проще. Для SwiftUI-приложений с NavigationStack — тоже избыточен. Coordinator оправдан для приложений с 15+ экранами, сложными flow (онбординг с ветвлениями, авторизация с восстановлением пароля) и mixed UIKit/SwiftUI проектами.

Как тестировать Coordinator?

Mock Router — проверка, какие методы вызываются и с какими параметрами. Проверка childCoordinators: после start() массив не пустой, после finish() — пустой. Coordinator тестируется без UI: Router — протокол, его mock не требует UIKit. Используйте XCTestExpectation для async flow. В Android аналоги — тестирование NavigationController и NavHost с mock-навигацией.

Итоги

  • Coordinator — паттерн навигации, выносящий переходы из ViewController в отдельный класс
  • Router — абстракция UINavigationController, используемая Coordinator'ом
  • Иерархия — родительские и дочерние Coordinator с делегированием результатов
  • MVVM-C — стандартный подход для UIKit проектов с Coordinator
  • SwiftUI — встроенная навигация через NavigationStack заменяет Coordinator
  • Тестирование — Coordinator тестируется через mock Router без UIKit

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также