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 (онбординг з розгалуженнями, авторизація з відновленням пароля) та змішаними 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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