Coordinator (koordinátor) — egy architekturális navigációs minta, amely a képernyők közötti átmenetek logikáját a ViewController-ból külön osztályokba helyezi át. A mintát Soroush Khanlou javasolta 2015-ben, és széles körben elterjedt az iOS közösségben. A Coordinator kezeli az alkalmazás flow-ját: létrehozza és megjeleníti a ViewController-t, adatokat továbbít a képernyők között és feldolgozza a flow befejezését. A minta megoldja a Massive View Controller problémát azáltal, hogy kiveszi a navigációt a kontrollerből. Bővebben — a eredeti Coordinator cikkben.
Főbb pontok
Coordinator — egy minta, amely átveszi a navigáció felelősségét egy iOS alkalmazásban. A szabványos UIKit-ben a ViewController maga kezeli az átmeneteket: present, push, show segue — az összes navigációs metódus az UIViewController-ből kerül meghívásra. A Coordinator kiveszi ezt a logikát: a ViewController jelent egy eseményt (pl. „a felhasználó megnyomta a bejelentkezés gombot”), a Coordinator eldönti, melyik képernyőt mutassa meg következőként. A ViewController csak az UI-logikával marad, és a navigációt a koordinátorra delegálja.
A minta szerkezete — CoordinatorProtocol start() és finish() metódusokkal. start() — a flow kezdete: az első ViewController létrehozása és megjelenítése. finish() — a flow befejezése a szülő koordinátor értesítésével. Router — egy burkoló a UINavigationController (vagy UISplitViewController) körül, amely show, push, pop, dismiss metódusokat biztosít. A koordinátor nem közvetlenül a UINavigationController-rel dolgozik — csak a Router-en keresztül. Ez lehetővé teszi a navigáció tesztelését és az UI-keretrendszer cseréjét.
| Komponens | Szerep | Példa |
|---|---|---|
| Coordinator | Navigációs flow kezelése | AuthCoordinator, ProfileCoordinator |
| Router | Absztrakció a UINavigationController fölött | push, present, pop, dismiss |
| ViewController | UI + események delegálása a Coordinator-nak | LoginViewController.delegate |
Problémák, amelyeket a Coordinator megold — Massive View Controller (a navigáció — a kontroller elburjánzásának gyakori oka). A szabványos UIKit-ben a ViewController tartalmazza a prepareForSegue-t, navigációs delegáltakat, unwind segues feldolgozást. A Coordinator ezt megszünteti. A Segue a storyboard-ban — statikus kapcsolat a képernyők között, a Coordinator dinamikus navigációt biztosít feltételekkel. A navigáció tesztelése lehetővé válik: a Coordinator tesztelhető UI nélkül a Router-hívások sorrendjének ellenőrzésével.
Alap Coordinator Swift-ben — egy protokoll társított típussal a Router számára és start/finish metódusokkal. Router — egy protokoll, amely absztrahálja a UINavigationController-t. A Router konkrét implementációja becsomagolja a UINavigationController-t és delegálja neki a metódusokat. A Coordinator a Router-t az init-ben fogadja és navigációra használja. A gyermek koordinátorok a childCoordinators tömbben tárolódnak az életciklus kezeléséhez.
// Router — navigáció absztrakció
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 kezelés
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 létrehozása az AppDelegate/SceneDelegate-ben — az AppDelegate vagy SceneDelegate létrehozza a UINavigationController-t, becsomagolja NavigationRouter-be, létrehozza a gyökér Coordinator-t (AppCoordinator) és meghívja a start()-ot. Az AppCoordinator eldönti, hogy megjelenítse-e az onboarding-ot, bejelentkezést vagy a főképernyőt — az alkalmazás állapotától függően. Coordinator — az egyetlen belépési pont a navigációhoz, a ViewController nem tud más képernyőkről.
Koordinátorok hierarchiája — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. A gyermek koordinátort a szülő hozza létre és tárolja a childCoordinators tömbben. Amikor a gyermek koordinátor befejezi a munkáját, meghívja a finish()-t a szülőnél, és a szülő eltávolítja a childCoordinators-ből. Ez megakadályozza a memóriaszivárgást: a Coordinator erős hivatkozással rendelkezik a ViewController-re (a Router-en keresztül), és anélkül, hogy eltávolítanák a childCoordinators-ből, az objektum nem szabadul fel.
// Delegált a Coordinator -> Parent kapcsolathoz
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — szülő
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 kezelése — a Coordinator eltávolítása a tömbből az egyetlen módja a felszabadításának. Ha elfelejti eltávolítani a befejezett Coordinator-t, az a memóriában marad a ViewController-ekkel együtt. Javasolt: a szülő didMove(toParent:), callback a befejezéskor, vagy Combine publisher az automatikus eltávolításhoz. A Coordinator minta nem specifikálja az értesítési mechanizmust — delegate, closure vagy Combine — a választás a fejlesztőé.
Adatátvitel delegált segítségével — a gyermek Coordinator definiál egy delegate protokollt metódusokkal, amelyeken keresztül az eredmények továbbítódnak: func authCoordinator(_:didLoginWith user: User). A szülő implementálja a protokollt, és adatokat kap a gyermek flow befejezésekor. Ez típusbiztos és explicit. Hátrány: minden gyermek Coordinator-hoz külön protokollt kell írni. 10+ Coordinator-t tartalmazó projekteknél ez a fájlok számának növekedéséhez vezet.
Adatátvitel Result típus segítségével — a finish metódus Result
| Módszer | Előnyök | Hátrányok |
|---|---|---|
| Delegate | Típusbiztos, explicit, külön protokollok | Sok protokoll, sok boilerplate |
| Closure | Kompakt, kevesebb fájl | Nehéz debugolni a retain cycle-t |
| Combine/Rx | Reaktív, könnyen kombinálható | Függőség a könyvtártól, nehezebb Debug |
Megosztott adatréteg — A Coordinator-ok nem közvetlenül továbbítanak adatokat, hanem közös szolgáltatást/adattárat használnak. Az AuthCoordinator elmenti a token-t a Keychain/UserDefaults-ba, a ProfileCoordinator onnan olvassa. A Coordinator-ok közös állapoton (Dependency Injection konténer) keresztül kommunikálnak, nem közvetlen hívásokkal. Ez csökkenti a Coordinator-ok összekapcsoltságát, de implicit függőségeket hoz létre a közös állapottól.
Coordinator vs Router — A Router a Coordinator egyik komponense, amely absztrahálja a UINavigationController-t. A Coordinator felelős a flow-ért (melyik képernyőt mutassa), a Router — a mechanikáért (hogyan mutassa: push/present). A Router a „hogyan”, a Coordinator a „mit”. A Router használható Coordinator nélkül (pl. Navigator-szingleton), de a Coordinator Router nélkül — csak egy ViewController más absztrakcióval. Általában mindkét mintát együtt használják.
Coordinator vs VIPER — A VIPER rendelkezik egy Wireframe komponenssel, amely a navigációért felelős — a Coordinator analógja. A VIPER-ben a Wireframe a modul része, a Coordinator — külön réteg a modulok fölött. A VIPER modul (View-Interactor-Presenter-Entity-Router) tartalmazza a navigációt a modul részeként. A Coordinator külső a modulokhoz képest: létrehozza és összekapcsolja a modulokat, de nem része azok összetételének. A Coordinator rugalmasabb a képernyők különböző flow-kban való újrafelhasználásához.
MVVM-C — az MVVM kiterjesztése Coordinator-ral. A ViewModel nem ismeri közvetlenül a Coordinator-t — a ViewController a ViewModel-en keresztül delegálja a navigációt, a ViewModel egy protokollon keresztül hívja meg a koordinátort. MVVM-C — a szabványos megközelítés iOS projektekhez SwiftUI-vel: a Coordinator kezeli a NavigationStack-t vagy a fullScreenCover-t, a ViewModel állapot közzétételével hívja meg a koordinátort. Az Apple nem javasolja a Coordinator-t SwiftUI-hez — a NavigationStack és NavigationPath beépített navigációs mechanizmusok.
// MVVM-C: ViewModel protokollon keresztül hívja a Coordinator-t
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// logika...
navigation?.showMainScreen()
}
}
Gyakran ismételt kérdések
A SwiftUI esetében a beépített navigáció (NavigationStack, NavigationPath) gyakran helyettesíti a Coordinator-t. Az Apple a path-alapú navigációt ajánlja. A Coordinator összetett, mély feltételekkel rendelkező flow-k esetén van értelme (onboarding-bejelentkezés-főképernyő a szereptől függően). Egyszerű SwiftUI alkalmazásokhoz a Coordinator felesleges — használja a NavigationPath-et.
Nem, ezek különböző minták. A Coordinator kezeli a navigációs flow-t: eldönti, melyik képernyőt mutassa, létrehozza a ViewController-eket és összekapcsolja őket. A Router — absztrakció a UINavigationController fölött: push, present, pop, dismiss. A Coordinator a Router-t használja a navigáció végrehajtásához. Néhány implementációban a Router tartalmazza a Coordinator logikáját (Router-per-screen), de ez eltérés az eredeti mintától.
Két fő szivárgási hely: childCoordinators (a szülő tartja a gyermeket, elfelejti eltávolítani) és Router (a UINavigationController tartja a ViewController-t). Megoldás: mindig távolítsa el a gyermek Coordinator-t a tömbből finish()-nél. Használjon weak referenciát a delegate-hez. Router esetén — ne tartson erős referenciát a UINavigationController-re, ha már a window hierarchiában van. Tesztelje a Coordinator deinit-jét.
3-5 képernyős alkalmazásokhoz a Coordinator felesleges — a segue vagy egyszerű navigationController.pushViewController egyszerűbb. NavigationStack-kel rendelkező SwiftUI alkalmazásokhoz is felesleges. A Coordinator 15+ képernyős, összetett flow-val (elágazó onboarding, jelszó-visszaállítással rendelkező hitelesítés) és vegyes UIKit/SwiftUI projektekhez indokolt.
Mock Router — annak ellenőrzése, hogy mely metódusok milyen paraméterekkel kerülnek meghívásra. childCoordinators ellenőrzése: start() után a tömb nem üres, finish() után — üres. A Coordinator UI nélkül tesztelhető: a Router egy protokoll, a mock-ja nem igényel UIKit-et. Használjon XCTestExpectation-t aszinkron flow-hoz. Android-ban analóg — a NavigationController és NavHost tesztelése mock-navigációval.
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is