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Апстракција над UINavigationController-омpush, present, pop, dismiss
ViewControllerUI + делегирање догађаја Coordinator-уLoginViewController.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:) родитеља, повратни позив при завршетку или 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. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође