Coordinator (koordinator) — ett arkitektoniskt navigeringsmönster som flyttar logiken för övergångar mellan skärmar från ViewController till separata klasser. Mönstret föreslogs av Soroush Khanlou 2015 och blev brett spridd inom iOS-communityt. Coordinator hanterar applikationens flöde: skapar och visar ViewController, överför data mellan skärmar och bearbetar slutförandet av flödet. Mönstret löser problemet med Massive View Controller genom att flytta ut navigeringen från kontrollern. Mer — i originalartikeln om Coordinator.
Huvudpunkter
Coordinator — ett mönster som tar över ansvaret för navigering i en iOS-applikation. I standard UIKit hanterar ViewController själv övergångarna: present, push, show segue — alla navigeringsmetoder anropas från UIViewController. Coordinator flyttar ut denna logik: ViewController rapporterar en händelse (t.ex. „användaren tryckte på inloggningsknappen“), Coordinator bestämmer vilken skärm som ska visas härnäst. ViewController blir bara kvar med UI-logik och delegerar navigeringen till koordinatorn.
Struktur för mönstret — CoordinatorProtocol med metoder start() och finish(). start() — början av flödet: skapa den första ViewController och visa den. finish() — slutförande av flödet med meddelande till föräldrakoordinatorn. Router — ett omslag runt UINavigationController (eller UISplitViewController) som tillhandahåller metoderna show, push, pop, dismiss. Koordinatorn arbetar inte direkt med UINavigationController — endast via Router. Detta gör det möjligt att testa navigering och byta UI-ramverk.
| Komponent | Roll | Exempel |
|---|---|---|
| Coordinator | Hantering av navigeringsflöde | AuthCoordinator, ProfileCoordinator |
| Router | Abstraktion över UINavigationController | push, present, pop, dismiss |
| ViewController | UI + delegering av händelser till Coordinator | LoginViewController.delegate |
Problem som Coordinator löser — Massive View Controller (navigering — vanlig orsak till kontrollertillväxt). I standard UIKit innehåller ViewController prepareForSegue, navigeringsdelegater, bearbetning av unwind segues. Coordinator eliminerar detta. Segue i storyboard — en statisk koppling mellan skärmar, Coordinator ger dynamisk navigering med villkor. Testning av navigering blir möjlig: Coordinator kan testas utan UI genom att kontrollera ordningen för Router-anrop.
Grundläggande Coordinator i Swift — ett protokoll med en associerad typ för Router och metoder start/finish. Router — ett protokoll som abstraherar UINavigationController. Den konkreta implementeringen av Router omsluter UINavigationController och delegerar metoder till den. Coordinator tar emot Router i init och använder den för navigering. Barnkoordinatorer lagras i arrayen childCoordinators för livscykelhantering.
// Router — navigeringsabstraktion
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 — flödeshantering
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)
}
}
Skapa Coordinator i AppDelegate/SceneDelegate — AppDelegate eller SceneDelegate skapar UINavigationController, omsluter den i NavigationRouter, skapar rot-Coordinator (AppCoordinator) och anropar start(). AppCoordinator bestämmer om onboarding, inloggning eller huvudskärm ska visas — beroende på applikationens tillstånd. Coordinator — den enda ingångspunkten för navigering, ViewController vet inte om andra skärmar.
Hierarki av koordinatorer — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Barnkoordinatorn skapas av föräldern och lagras i arrayen childCoordinators. När barnkoordinatorn slutför sitt arbete anropar den finish() hos föräldern, och föräldern tar bort den från childCoordinators. Detta förhindrar minnesläckor: Coordinator har en stark referens till ViewController (via Router), och utan borttagning från childCoordinators frigörs inte objektet.
// Delegat för kommunikation Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — förälder
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()
}
}
Hantering av childCoordinators — borttagning av Coordinator från arrayen är det enda sättet att frigöra den. Om du glömmer att ta bort en slutförd Coordinator stannar den kvar i minnet tillsammans med ViewControllers. Rekommenderas: förälderns didMove(toParent:), callback vid slutförande eller Combine publisher för automatisk borttagning. Coordinator-mönstret specificerar inte meddelandemekanismen — delegate, closure eller Combine — valet är utvecklarens.
Dataöverföring via delegat — barn-Coordinatorn definierar ett delegatprotokoll med metoder genom vilka resultaten överförs: func authCoordinator(_:didLoginWith user: User). Föräldern implementerar protokollet och tar emot data vid slutförandet av barnflödet. Detta är typsäkert och explicit. Nackdel: för varje barn-Coordinator måste ett separat protokoll skrivas. För projekt med 10+ Coordinatorer leder detta till en ökning av antalet filer.
Dataöverföring via Result-typ — metoden finish tar emot Result
| Metod | Fördelar | Nackdelar |
|---|---|---|
| Delegate | Typsäkert, explicit, separata protokoll | Många protokoll, mycket boilerplate |
| Closure | Kompakt, färre filer | Svårt att felsöka retain cycle |
| Combine/Rx | Reaktivt, lätt att kombinera | Beroende av bibliotek, svårare Debug |
Delad datalager — Coordinatorer överför inte data direkt, utan använder en gemensam tjänst/databas. AuthCoordinator sparar token i Keychain/UserDefaults, ProfileCoordinator läser därifrån. Coordinatorer kommunicerar via gemensamt tillstånd (Dependency Injection-behållare), inte via direkta anrop. Detta minskar kopplingen mellan Coordinatorer, men skapar implicita beroenden av gemensamt tillstånd.
Coordinator vs Router — Router är en komponent i Coordinator som abstraherar UINavigationController. Coordinator ansvarar för flödet (vilken skärm som ska visas), Router — för mekaniken (hur den visas: push/present). Router är „hur“, Coordinator är „vad“. Router kan användas utan Coordinator (t.ex. Navigator-singleton), men Coordinator utan Router — bara en ViewController med en annan abstraktion. Vanligtvis används båda mönstren tillsammans.
Coordinator vs VIPER — VIPER har en komponent Wireframe som ansvarar för navigering — analog till Coordinator. I VIPER är Wireframe en del av modulen, Coordinator — ett separat lager ovanför modulerna. VIPER-modulen (View-Interactor-Presenter-Entity-Router) inkluderar navigering som en del av modulen. Coordinator är extern i förhållande till modulerna: den skapar och kopplar samman moduler, men ingår inte i deras sammansättning. Coordinator är mer flexibel för återanvändning av skärmar i olika flöden.
MVVM-C — utvidgning av MVVM med Coordinator. ViewModel känner inte direkt till Coordinator — ViewController delegerar navigering via ViewModel, ViewModel anropar koordinatorn via ett protokoll. MVVM-C — standardmetoden för iOS-projekt med SwiftUI: Coordinator hanterar NavigationStack eller fullScreenCover, ViewModel anropar koordinatorn genom att publicera tillstånd. Apple rekommenderar inte Coordinator för SwiftUI — NavigationStack och NavigationPath är inbyggda navigeringsmekanismer.
// MVVM-C: ViewModel anropar Coordinator via protokoll
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// logik...
navigation?.showMainScreen()
}
}
Vanliga frågor
För SwiftUI ersätter inbyggd navigering (NavigationStack, NavigationPath) ofta Coordinator. Apple rekommenderar path-baserad navigering. Coordinator är meningsfullt för komplexa flöden med djupa villkor (onboarding-inloggning-huvudskärm beroende på roll). För enkla SwiftUI-applikationer är Coordinator överflödig — använd NavigationPath.
Nej, det är olika mönster. Coordinator hanterar navigeringsflödet: bestämmer vilken skärm som ska visas, skapar ViewController och kopplar samman dem. Router — abstraktion över UINavigationController: push, present, pop, dismiss. Coordinator använder Router för att utföra navigering. I vissa implementeringar inkluderar Router Coordinator-logik (Router-per-screen), men detta är en avvikelse från det ursprungliga mönstret.
Två huvudsakliga läckageplatser: childCoordinators (förälder håller barnet, glömmer att ta bort) och Router (UINavigationController håller ViewController). Lösning: ta alltid bort barn-Coordinatorn från arrayen vid finish(). Använd weak-referens för delegate. För Router — håll inte en stark referens till UINavigationController om den redan är i window-hierarkin. Testa Coordinatorns deinit.
För applikationer med 3-5 skärmar är Coordinator överflödig — segue eller enkel navigationController.pushViewController är enklare. För SwiftUI-applikationer med NavigationStack — också överflödig. Coordinator är motiverad för applikationer med 15+ skärmar, komplexa flöden (onboarding med förgreningar, autentisering med lösenordsåterställning) och blandade UIKit/SwiftUI-projekt.
Mock Router — kontrollera vilka metoder som anropas och med vilka parametrar. Kontroll av childCoordinators: efter start() är arrayen inte tom, efter finish() — tom. Coordinator testas utan UI: Router är ett protokoll, dess mock kräver inte UIKit. Använd XCTestExpectation för asynkront flöde. I Android analogt — testning av NavigationController och NavHost med mock-navigering.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också