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 одлучује да прикаже onboarding, пријаву или главни екран — у зависности од стања апликације. 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:) родитеља, повратни позив при завршетку или Combine publisher за аутоматско уклањање. Образац Coordinator не специфицира механизам обавјештавања — делегат, closure или Combine — избор је на програмеру.
Пренос података путем делегата — дјечији Coordinator дефинише протокол делегата са методима кроз које се преносе резултати: func authCoordinator(_:didLoginWith user: User). Родитељ имплементира протокол и прима податке при завршетку дјечијег flow-а. Ово је типски безбједно и експлицитно. Недостатак: за сваког дјечијег Coordinator-а треба писати засебан протокол. За пројекте са 10+ Coordinator-а ово доводи до повећања броја датотека.
Пренос података путем Result типа — метод finish прима Result
| Метод | Предности | Недостаци |
|---|---|---|
| Delegate | Типски безбједно, експлицитно, засебни протоколи | Много протокола, много boilerplate-а |
| Closure | Компактно, мање датотека | Тешко отклањати retain cycle |
| Combine/Rx | Реактивно, лако комбиновати | Зависност од библиотеке, тежи Debug |
Заједнички слој података — Coordinator-и не преносе податке директно, већ користе заједнички сервис/репозиторијум. AuthCoordinator чува token у Keychain/UserDefaults, ProfileCoordinator чита одатле. Coordinator-и комуницирају путем заједничког стања (Dependency Injection контејнера), а не директним позивима. Ово смањује повезаност 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 позива координатора кроз протокол. MVVM-C — стандардни приступ за iOS пројекте са SwiftUI-јем: Coordinator управља NavigationStack-ом или fullScreenCover-ом, ViewModel позива координатора објављивањем стања. 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 навигацију. Coordinator има смисла за сложене flow-ове са дубоким условима (onboarding-пријава-главни екран у зависности од улоге). За једноставне апликације на 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-овима (onboarding са гранањем, ауторизација са опоравком лозинке) и мијешаним UIKit/SwiftUI пројектима.
Mock Router — провјера који методи се позивају и са којим параметрима. Провјера childCoordinators: након start() низ није празан, након finish() — празан. Coordinator се тестира без UI-ја: Router је протокол, његов mock не захтијева UIKit. Користите XCTestExpectation за асинхрони flow. У Android-у аналогно — тестирање NavigationController-а и NavHost-а са mock-навигацијом.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође