Coordinator (koordynator) — architektoniczny wzorzec nawigacji, który przenosi logikę przejść między ekranami z ViewController do oddzielnych klas. Wzorzec zaproponowany przez Sorousha Khanlou w 2015 roku i zyskał szeroką popularność w społeczności iOS. Coordinator zarządza flow aplikacji: tworzy i wyświetla ViewController, przekazuje dane między ekranami i obsługuje zakończenie flow. Wzorzec rozwiązuje problem Massive View Controller, przenosząc nawigację z kontrolera. Więcej — w oryginalnym artykule Coordinator.
Najważniejsze
Coordinator — wzorzec, który przejmuje odpowiedzialność za nawigację w aplikacji iOS. W standardowym UIKit ViewController sam zarządza przejściami: present, push, show segue — wszystkie metody nawigacji wywoływane są z UIViewController. Coordinator przenosi tę logikę: ViewController informuje o zdarzeniu (np. „użytkownik nacisnął przycisk logowania”), Coordinator decyduje, który ekran pokazać następnie. ViewController pozostaje tylko z logiką UI i deleguje nawigację do koordynatora.
Struktura wzorca — CoordinatorProtocol z metodami start() i finish(). start() — rozpoczęcie flow: utworzenie pierwszego ViewController i wyświetlenie. finish() — zakończenie flow z powiadomieniem rodzica. Router — otoczka nad UINavigationController (lub UISplitViewController), udostępniająca metody show, push, pop, dismiss. Koordynator nie pracuje bezpośrednio z UINavigationController — tylko przez Router. Pozwala to testować nawigację i przełączać framework UI.
| Komponent | Rola | Przykład |
|---|---|---|
| Coordinator | Zarządzanie flow nawigacji | AuthCoordinator, ProfileCoordinator |
| Router | Abstrakcja nad UINavigationController | push, present, pop, dismiss |
| ViewController | UI + delegacja zdarzeń do Coordinator | LoginViewController.delegate |
Problemy, które rozwiązuje Coordinator — Massive View Controller (nawigacja — częsta przyczyna rozrastania się kontrolera). W standardowym UIKit ViewController zawiera prepareForSegue, delegaty nawigacji, obsługę unwind segues. Coordinator eliminuje to. Segue w storyboard — statyczne połączenie między ekranami, Coordinator zapewnia dynamiczną nawigację z warunkami. Testowanie nawigacji staje się możliwe: można przetestować Coordinator bez UI, sprawdzając sekwencję wywołań Router.
Podstawowy Coordinator w Swift — protokół z typem stowarzyszonym dla Router i metodami start/finish. Router — protokół abstrahujący UINavigationController. Konkretna implementacja Router otacza UINavigationController i deleguje mu metody. Coordinator przyjmuje Router w init i używa go do nawigacji. Dziecięce koordynatory przechowywane są w tablicy childCoordinators do zarządzania cyklem życia.
// Router — abstrakcja nawigacji
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 — zarządzanie 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)
}
}
Tworzenie Coordinator w AppDelegate/SceneDelegate — AppDelegate lub SceneDelegate tworzy UINavigationController, otacza go w NavigationRouter, tworzy główny Coordinator (AppCoordinator) i wywołuje start(). AppCoordinator decyduje, czy pokazać onboarding, logowanie czy główny ekran — w zależności od stanu aplikacji. Coordinator — jedyny punkt wejścia dla nawigacji, ViewController nie wie o innych ekranach.
Hierarchia koordynatorów — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Dziecięcy koordynator tworzony jest przez rodzica i przechowywany w tablicy childCoordinators. Gdy dziecięcy koordynator kończy pracę, wywołuje finish() u rodzica, a rodzic usuwa go z childCoordinators. Zapobiega to wyciekom pamięci: Coordinator ma silne referencje do ViewController (przez Router), i bez usunięcia z childCoordinators obiekt nie zostanie zwolniony.
// Delegat do komunikacji Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — rodzic
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()
}
}
Zarządzanie childCoordinators — usunięcie Coordinatora z tablicy to jedyny sposób na jego zwolnienie. Jeśli zapomnisz usunąć zakończony Coordinator, pozostaje on w pamięci wraz z ViewControllerami. Zalecane: didMove(toParent:) rodzica, callback przy zakończeniu lub Combine publisher do automatycznego usuwania. Wzorzec Coordinator nie specyfikuje mechanizmu powiadamiania — delegat, closure lub Combine — wybór należy do programisty.
Przekazywanie danych przez delegata — dziecięcy Coordinator definiuje protokół delegata z metodami, przez które przekazywane są wyniki: func authCoordinator(_:didLoginWith user: User). Rodzic implementuje protokół i otrzymuje dane przy zakończeniu dziecięcego flow. Jest to typowo bezpieczne i jawne. Wada: dla każdego dziecięcego Coordinatora trzeba pisać osobny protokół. Dla projektów z 10+ Coordinatorami prowadzi to do wzrostu liczby plików.
Przekazywanie danych przez typ Result — metoda finish przyjmuje Result
| Metoda | Zalety | Wady |
|---|---|---|
| Delegate | Typowo bezpieczne, jawne, osobne protokoły | Dużo protokołów, dużo boilerplate'u |
| Closure | Kompaktowe, mniej plików | Trudno debugować retain cycle |
| Combine/Rx | Reaktywne, łatwo łączyć | Zależność od biblioteki, trudniejszy Debug |
Shared data layer — Coordinatory nie przekazują danych bezpośrednio, ale używają wspólnego serwisu/repozytorium. AuthCoordinator zapisuje token w Keychain/UserDefaults, ProfileCoordinator czyta stamtąd. Coordinatory komunikują się przez wspólny stan (Dependency Injection container), a nie przez bezpośrednie wywołania. Zmniejsza to powiązanie Coordinatorów, ale tworzy niejawne zależności od wspólnego stanu.
Coordinator vs Router — Router to komponent Coordinatora, abstrahujący UINavigationController. Coordinator odpowiada za flow (który ekran pokazać), Router — za mechanikę (jak pokazać: push/present). Router to „jak”, Coordinator to „co”. Można używać Router bez Coordinator (np. Navigator-singleton), ale Coordinator bez Router — to tylko ViewController z inną abstrakcją. Zazwyczaj oba wzorce są używane razem.
Coordinator vs VIPER — VIPER ma komponent Wireframe odpowiedzialny za nawigację — analog Coordinatora. W VIPER Wireframe jest częścią modułu, Coordinator — oddzielną warstwą nad modułami. Moduł VIPER (View-Interactor-Presenter-Entity-Router) zawiera nawigację jako część modułu. Coordinator jest zewnętrzny względem modułów: tworzy i łączy moduły, ale nie wchodzi w ich skład. Coordinator jest bardziej elastyczny do ponownego użycia ekranów w różnych flow.
MVVM-C — rozszerzenie MVVM o Coordinator. ViewModel nie zna bezpośrednio Coordinatora — ViewController deleguje nawigację przez ViewModel, ViewModel wywołuje koordynatora przez protokół. MVVM-C — standardowe podejście dla projektów iOS z SwiftUI: Coordinator zarządza NavigationStack lub fullScreenCover, ViewModel wywołuje koordynatora przez publikację stanu. Apple nie zaleca Coordinatora dla SwiftUI — NavigationStack i NavigationPath to wbudowane mechanizmy nawigacji.
// MVVM-C: ViewModel wywołuje Coordinator przez protokół
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// logika...
navigation?.showMainScreen()
}
}
Często zadawane pytania
Dla SwiftUI wbudowana nawigacja (NavigationStack, NavigationPath) często zastępuje Coordinator. Apple zaleca path-based navigation. Coordinator ma sens dla złożonych flow z głębokimi warunkami (onboarding-logowanie-ekran główny w zależności od roli). Dla prostych aplikacji w SwiftUI Coordinator jest zbędny — użyj NavigationPath.
Nie, to różne wzorce. Coordinator zarządza flow nawigacji: decyduje, który ekran pokazać, tworzy ViewController i łączy je. Router to abstrakcja nad UINavigationController: push, present, pop, dismiss. Coordinator używa Router do wykonania nawigacji. W niektórych implementacjach Router zawiera logikę Coordinator (Router-per-screen), ale to odejście od oryginalnego wzorca.
Dwa główne miejsca wycieków: childCoordinators (rodzic trzyma dziecięcego, zapominając usunąć) i Router (UINavigationController trzyma ViewController). Rozwiązanie: zawsze usuwać dziecięcy Coordinator z tablicy przy finish(). Używać weak referencji dla delegate. Dla Router — nie trzymać silnej referencji do UINavigationController, jeśli jest już w hierarchii window. Testuj deinit Coordinatora.
Dla aplikacji z 3-5 ekranami Coordinator jest zbędny — segue lub prosty navigationController.pushViewController są prostsze. Dla aplikacji SwiftUI z NavigationStack — też zbędny. Coordinator jest uzasadniony dla aplikacji z 15+ ekranami, złożonymi flow (onboarding z rozgałęzieniami, autoryzacja z odzyskiwaniem hasła) i mieszanymi projektami UIKit/SwiftUI.
Mock Router — sprawdzenie, które metody są wywoływane i z jakimi parametrami. Sprawdzenie childCoordinators: po start() tablica nie jest pusta, po finish() — pusta. Coordinator testowany jest bez UI: Router to protokół, jego mock nie wymaga UIKit. Użyj XCTestExpectation dla async flow. W Androidzie analogicznie — testowanie NavigationController i NavHost z mock-nawigacją.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również