Coordinator (koordinátor) — architektonický navigační vzor, který přenáší logiku přechodů mezi obrazovkami z ViewControlleru do samostatných tříd. Vzor navrhl Soroush Khanlou v roce 2015 a získal široké rozšíření v iOS komunitě. Coordinator spravuje flow aplikace: vytváří a zobrazuje ViewController, předává data mezi obrazovkami a zpracovává dokončení flow. Vzor řeší problém Massive View Controller tím, že navigaci vyjímá z kontroléru. Více — v původním článku o Coordinatorovi.
Hlavní body
Coordinator — vzor, který přebírá odpovědnost za navigaci v iOS aplikaci. Ve standardním UIKit si ViewController spravuje přechody sám: present, push, show segue — všechny navigační metody jsou volány z UIViewController. Coordinator tuto logiku přenáší: ViewController hlásí událost (např. „uživatel stiskl tlačítko přihlášení“), Coordinator rozhoduje, která obrazovka se zobrazí dále. ViewController zůstává pouze s UI logikou a deleguje navigaci koordinátorovi.
Struktura vzoru — CoordinatorProtocol s metodami start() a finish(). start() — začátek flow: vytvoření prvního ViewControlleru a zobrazení. finish() — dokončení flow s oznámením rodičovskému koordinátorovi. Router — obal kolem UINavigationControlleru (nebo UISplitViewControlleru), který poskytuje metody show, push, pop, dismiss. Koordinátor nepracuje přímo s UINavigationControllerem — pouze přes Router. To umožňuje testovat navigaci a přepínat UI framework.
| Komponenta | Role | Příklad |
|---|---|---|
| Coordinator | Správa navigačního flow | AuthCoordinator, ProfileCoordinator |
| Router | Abstrakce nad UINavigationControllerem | push, present, pop, dismiss |
| ViewController | UI + delegování událostí Coordinatoru | LoginViewController.delegate |
Problémy, které Coordinator řeší — Massive View Controller (navigace — častá příčina rozrůstání kontroléru). Ve standardním UIKit obsahuje ViewController prepareForSegue, delegáty navigace, zpracování unwind segues. Coordinator to eliminuje. Segue ve storyboardu — statické propojení mezi obrazovkami, Coordinator poskytuje dynamickou navigaci s podmínkami. Testování navigace je možné: Coordinator lze testovat bez UI kontrolou posloupnosti volání Routeru.
Základní Coordinator ve Swiftu — protokol s asociovaným typem pro Router a metodami start/finish. Router — protokol abstrahující UINavigationController. Konkrétní implementace Routeru obaluje UINavigationController a deleguje mu metody. Coordinator přijímá Router v init a používá jej pro navigaci. Dceřiné koordinátory jsou uloženy v poli childCoordinators pro správu životního cyklu.
// Router — abstrakce navigace
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 — správa 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)
}
}
Vytvoření Coordinatoru v AppDelegate/SceneDelegate — AppDelegate nebo SceneDelegate vytvoří UINavigationController, obalí jej do NavigationRouteru, vytvoří kořenový Coordinator (AppCoordinator) a zavolá start(). AppCoordinator rozhodne, zda zobrazit onboarding, přihlášení nebo hlavní obrazovku — v závislosti na stavu aplikace. Coordinator — jediný vstupní bod pro navigaci, ViewController neví o ostatních obrazovkách.
Hierarchie koordinátorů — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Dceřiný koordinátor je vytvořen rodičem a uložen v poli childCoordinators. Když dceřiný koordinátor dokončí práci, zavolá finish() u rodiče a rodič jej odstraní z childCoordinators. To zabraňuje únikům paměti: Coordinator má silný odkaz na ViewController (přes Router) a bez odstranění z childCoordinators objekt nebude uvolněn.
// Delegát pro komunikaci Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — rodič
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()
}
}
Správa childCoordinators — odstranění Coordinatoru z pole je jediný způsob, jak jej uvolnit. Pokud zapomenete odstranit dokončený Coordinator, zůstává v paměti spolu s ViewControllery. Doporučuje se: didMove(toParent:) rodiče, callback při dokončení nebo Combine publisher pro automatické odstranění. Vzor Coordinator nespecifikuje mechanismus oznámení — delegát, closure nebo Combine — volba je na vývojáři.
Předávání dat prostřednictvím delegáta — dceřiný Coordinator definuje protokol delegáta s metodami, přes které se předávají výsledky: func authCoordinator(_:didLoginWith user: User). Rodič implementuje protokol a přijímá data při dokončení dceřiného flow. To je typově bezpečné a explicitní. Nevýhoda: pro každý dceřiný Coordinator je třeba napsat samostatný protokol. U projektů s 10+ Coordinatory to vede k nárůstu počtu souborů.
Předávání dat přes typ Result — metoda finish přijímá Result
| Metoda | Výhody | Nevýhody |
|---|---|---|
| Delegate | Typově bezpečné, explicitní, samostatné protokoly | Hodně protokolů, hodně boilerplate |
| Closure | Kompaktní, méně souborů | Obtížné ladění retain cycle |
| Combine/Rx | Reaktivní, snadno kombinovatelné | Závislost na knihovně, obtížnější Debug |
Sdílená datová vrstva — Coordinatory nepředávají data přímo, ale používají sdílenou službu/repozitář. AuthCoordinator ukládá token do Keychain/UserDefaults, ProfileCoordinator odtud čte. Coordinatory komunikují prostřednictvím sdíleného stavu (kontejner Dependency Injection), nikoli přímými voláními. To snižuje provázanost Coordinatorů, ale vytváří implicitní závislosti na sdíleném stavu.
Coordinator vs Router — Router je komponenta Coordinatoru, která abstrahuje UINavigationController. Coordinator odpovídá za flow (kterou obrazovku zobrazit), Router — za mechaniku (jak zobrazit: push/present). Router je „jak“, Coordinator je „co“. Router lze použít bez Coordinatoru (např. Navigator-singleton), ale Coordinator bez Routeru — jen ViewController s jinou abstrakcí. Obvykle se oba vzory používají společně.
Coordinator vs VIPER — VIPER má komponentu Wireframe odpovědnou za navigaci — analog Coordinatoru. Ve VIPERu je Wireframe součástí modulu, Coordinator — samostatná vrstva nad moduly. Modul VIPER (View-Interactor-Presenter-Entity-Router) zahrnuje navigaci jako součást modulu. Coordinator je vůči modulům externí: vytváří a propojuje moduly, ale není součástí jejich složení. Coordinator je flexibilnější pro opětovné použití obrazovek v různých flow.
MVVM-C — rozšíření MVVM o Coordinator. ViewModel nezná Coordinator přímo — ViewController deleguje navigaci přes ViewModel, ViewModel volá koordinátora přes protokol. MVVM-C — standardní přístup pro iOS projekty se SwiftUI: Coordinator spravuje NavigationStack nebo fullScreenCover, ViewModel volá koordinátora publikováním stavu. Apple nedoporučuje Coordinator pro SwiftUI — NavigationStack a NavigationPath jsou vestavěné navigační mechanismy.
// MVVM-C: ViewModel volá Coordinator přes protokol
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// logika...
navigation?.showMainScreen()
}
}
Často kladené otázky
Pro SwiftUI vestavěná navigace (NavigationStack, NavigationPath) často nahrazuje Coordinator. Apple doporučuje path-based navigaci. Coordinator má smysl pro komplexní flow s hlubokými podmínkami (onboarding-přihlášení-hlavní obrazovka v závislosti na roli). Pro jednoduché aplikace na SwiftUI je Coordinator nadbytečný — použijte NavigationPath.
Ne, jsou to různé vzory. Coordinator spravuje navigační flow: rozhoduje, kterou obrazovku zobrazit, vytváří ViewControllery a propojuje je. Router — abstrakce nad UINavigationControllerem: push, present, pop, dismiss. Coordinator používá Router k provedení navigace. V některých implementacích Router zahrnuje logiku Coordinatoru (Router-per-screen), ale to je odchylka od původního vzoru.
Dvě hlavní místa úniků: childCoordinators (rodič drží dceřiný, zapomene jej odstranit) a Router (UINavigationController drží ViewController). Řešení: vždy odstraňovat dceřiný Coordinator z pole při finish(). Použít weak referenci pro delegate. Pro Router — nedržet silný odkaz na UINavigationController, pokud je již v hierarchii window. Testujte deinit Coordinatoru.
Pro aplikace s 3-5 obrazovkami je Coordinator nadbytečný — segue nebo jednoduché navigationController.pushViewController je jednodušší. Pro aplikace SwiftUI s NavigationStack — také nadbytečný. Coordinator je ospravedlnitelný pro aplikace s 15+ obrazovkami, komplexními flow (onboarding s větvemi, autorizace s obnovením hesla) a smíšenými UIKit/SwiftUI projekty.
Mock Router — kontrola, které metody jsou volány a s jakými parametry. Kontrola childCoordinators: po start() pole není prázdné, po finish() — prázdné. Coordinator se testuje bez UI: Router je protokol, jeho mock nevyžaduje UIKit. Použijte XCTestExpectation pro asynchronní flow. V Androidu analogicky — testování NavigationController a NavHost s mock-navigací.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také