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Управление на навигационния flowAuthCoordinator, 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 решава да покаже onboarding, вход или главен екран — в зависимост от състоянието на приложението. 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 е генеричният тип на резултата от flow-а. Coordinator — генерик с асоцииран тип на резултата. start() с callback: start(completion: @escaping (Output) -> Void). Това съкращава кода: няма нужда от отделен протокол за всеки координатор. RxSwift/Combine: Coordinator публикува резултата чрез PassthroughSubject/Publisher. Изборът зависи от архитектурния подход на екипа.

МетодПредимстваНедостатъци
DelegateТипобезопасно, изрично, отделни протоколиМного протоколи, много boilerplate
ClosureКомпактно, по-малко файловеТрудно за дебъгване на retain cycle
Combine/RxРеактивно, лесно за комбиниранеЗависимост от библиотека, по-труден Debug

Споделен слой от данни — Coordinator-ите не предават данни директно, а използват обща услуга/репозиторий. AuthCoordinator записва token в Keychain/UserDefaults, ProfileCoordinator чете оттам. Coordinator-ите комуникират чрез споделено състояние (Dependency Injection контейнер), а не чрез директни извиквания. Това намалява свързаността на 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 извиква координатора чрез протокол. MVVM-C — стандартният подход за iOS проекти със SwiftUI: Coordinator управлява NavigationStack или fullScreenCover, ViewModel извиква координатора чрез публикуване на състояние. 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 навигация. Coordinator има смисъл за сложни flow с дълбоки условия (onboarding-вход-главен екран в зависимост от ролята). За прости 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 (onboarding с разклонения, авторизация с възстановяване на парола) и смесени UIKit/SwiftUI проекти.

Как да тестваме Coordinator?

Mock Router — проверка кои методи се извикват и с какви параметри. Проверка на childCoordinators: след start() масивът не е празен, след finish() — празен. Coordinator се тества без UI: Router е протокол, неговият mock не изисква UIKit. Използвайте XCTestExpectation за асинхронен 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 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също