Coordinator (coördinator) — een architectonisch navigatiepatroon dat de logica van overgangen tussen schermen uit ViewController naar aparte klassen verplaatst. Het patroon werd in 2015 voorgesteld door Soroush Khanlou en kreeg brede verspreiding in de iOS-gemeenschap. Coordinator beheert de flow van de applicatie: maakt ViewController aan en toont deze, geeft gegevens door tussen schermen en verwerkt de voltooiing van de flow. Het patroon lost het probleem van Massive View Controller op door navigatie uit de controller te halen. Meer — in het originele Coordinator-artikel.
Belangrijkste punten
Coordinator — een patroon dat de verantwoordelijkheid voor navigatie in een iOS-applicatie overneemt. In standaard UIKit beheert ViewController zelf de overgangen: present, push, show segue — alle navigatiemethoden worden aangeroepen vanuit UIViewController. Coordinator verplaatst deze logica: ViewController meldt een gebeurtenis (bijv. „gebruiker heeft op de inlogknop gedrukt“), Coordinator beslist welk scherm vervolgens wordt getoond. ViewController blijft alleen met UI-logica en delegeert navigatie aan de coördinator.
Structuur van het patroon — CoordinatorProtocol met methoden start() en finish(). start() — begin van de flow: aanmaken van de eerste ViewController en weergave. finish() — voltooiing van de flow met melding aan de ouder-coördinator. Router — een wrapper rond UINavigationController (of UISplitViewController) die methoden show, push, pop, dismiss biedt. De coördinator werkt niet direct met UINavigationController — alleen via Router. Dit maakt het mogelijk navigatie te testen en het UI-framework te wisselen.
| Component | Rol | Voorbeeld |
|---|---|---|
| Coordinator | Beheer van navigatie-flow | AuthCoordinator, ProfileCoordinator |
| Router | Abstractie over UINavigationController | push, present, pop, dismiss |
| ViewController | UI + delegeren van gebeurtenissen aan Coordinator | LoginViewController.delegate |
Problemen die Coordinator oplost — Massive View Controller (navigatie — veelvoorkomende oorzaak van controller-uitbreiding). In standaard UIKit bevat ViewController prepareForSegue, navigatie-delegates, verwerking van unwind segues. Coordinator elimineert dit. Segue in storyboard — een statische verbinding tussen schermen, Coordinator biedt dynamische navigatie met voorwaarden. Testen van navigatie wordt mogelijk: Coordinator kan zonder UI worden getest door de volgorde van Router-aanroepen te controleren.
Basis Coordinator in Swift — een protocol met een bijbehorend type voor Router en methoden start/finish. Router — een protocol dat UINavigationController abstraheert. De concrete implementatie van Router omhult UINavigationController en delegeert methoden eraan. Coordinator accepteert Router in init en gebruikt het voor navigatie. Kind-coördinatoren worden opgeslagen in de array childCoordinators voor levenscyclusbeheer.
// Router — abstractie van navigatie
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 — beheer van 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)
}
}
Aanmaken van Coordinator in AppDelegate/SceneDelegate — AppDelegate of SceneDelegate maakt UINavigationController aan, omhult het in NavigationRouter, maakt root-Coordinator (AppCoordinator) aan en roept start() aan. AppCoordinator beslist of onboarding, login of het hoofdscherm wordt getoond — afhankelijk van de applicatiestatus. Coordinator — het enige toegangspunt voor navigatie, ViewController weet niet van andere schermen.
Hiërarchie van coördinatoren — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. De kind-coördinator wordt door de ouder aangemaakt en opgeslagen in de array childCoordinators. Wanneer de kind-coördinator zijn werk voltooit, roept hij finish() aan bij de ouder en verwijdert de ouder hem uit childCoordinators. Dit voorkomt geheugenlekken: Coordinator heeft een sterke verwijzing naar ViewController (via Router), en zonder verwijdering uit childCoordinators wordt het object niet vrijgegeven.
// Delegate voor communicatie Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — ouder
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()
}
}
Beheer van childCoordinators — verwijdering van Coordinator uit de array is de enige manier om hem vrij te geven. Als u vergeet een voltooide Coordinator te verwijderen, blijft hij in het geheugen samen met de ViewControllers. Aanbevolen: didMove(toParent:) van de ouder, callback bij voltooiing of Combine publisher voor automatische verwijdering. Het Coordinator-patroon specificeert het meldingsmechanisme niet — delegate, closure of Combine — de keuze is aan de ontwikkelaar.
Gegevensoverdracht via delegate — de kind-Coordinator definieert een delegate-protocol met methoden waarmee resultaten worden doorgegeven: func authCoordinator(_:didLoginWith user: User). De ouder implementeert het protocol en ontvangt gegevens bij voltooiing van de kind-flow. Dit is typeveilig en expliciet. Nadeel: voor elke kind-Coordinator moet een apart protocol worden geschreven. Voor projecten met 10+ Coördinatoren leidt dit tot een toename van het aantal bestanden.
Gegevensoverdracht via Result-type — de methode finish accepteert Result
| Methode | Voordelen | Nadelen |
|---|---|---|
| Delegate | Typeveilig, expliciet, aparte protocollen | Veel protocollen, veel boilerplate |
| Closure | Compact, minder bestanden | Moeilijk te debuggen retain cycle |
| Combine/Rx | Reactief, makkelijk te combineren | Afhankelijkheid van bibliotheek, moeilijkere Debug |
Gedeelde gegevenslaag — Coördinatoren geven geen gegevens direct door, maar gebruiken een gedeelde service/repository. AuthCoordinator slaat token op in Keychain/UserDefaults, ProfileCoordinator leest van daar. Coördinatoren communiceren via gedeelde toestand (Dependency Injection-container), niet via directe aanroepen. Dit vermindert de koppeling van Coördinatoren, maar creëert impliciete afhankelijkheden van de gedeelde toestand.
Coordinator vs Router — Router is een component van Coordinator die UINavigationController abstraheert. Coordinator is verantwoordelijk voor de flow (welk scherm tonen), Router — voor de mechaniek (hoe tonen: push/present). Router is „hoe“, Coordinator is „wat“. Router kan zonder Coordinator worden gebruikt (bijv. Navigator-singleton), maar Coordinator zonder Router — slechts een ViewController met een andere abstractie. Meestal worden beide patronen samen gebruikt.
Coordinator vs VIPER — VIPER heeft een component Wireframe die verantwoordelijk is voor navigatie — analoog aan Coordinator. In VIPER is Wireframe onderdeel van de module, Coordinator — een aparte laag boven de modules. De VIPER-module (View-Interactor-Presenter-Entity-Router) omvat navigatie als onderdeel van de module. Coordinator is extern ten opzichte van modules: hij maakt en verbindt modules, maar maakt geen deel uit van hun samenstelling. Coordinator is flexibeler voor hergebruik van schermen in verschillende flows.
MVVM-C — uitbreiding van MVVM met Coordinator. ViewModel kent Coordinator niet direct — ViewController delegeert navigatie via ViewModel, ViewModel roept de coördinator aan via een protocol. MVVM-C — de standaardbenadering voor iOS-projecten met SwiftUI: Coordinator beheert NavigationStack of fullScreenCover, ViewModel roept de coördinator aan door statuspublicatie. Apple raadt Coordinator niet aan voor SwiftUI — NavigationStack en NavigationPath zijn ingebouwde navigatiemechanismen.
// MVVM-C: ViewModel roept Coordinator aan via protocol
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// logica...
navigation?.showMainScreen()
}
}
Veelgestelde vragen
Voor SwiftUI vervangt ingebouwde navigatie (NavigationStack, NavigationPath) vaak Coordinator. Apple beveelt path-based navigatie aan. Coordinator is zinvol voor complexe flows met diepe voorwaarden (onboarding-login-hoofdscherm afhankelijk van rol). Voor eenvoudige SwiftUI-applicaties is Coordinator overbodig — gebruik NavigationPath.
Nee, dit zijn verschillende patronen. Coordinator beheert de navigatie-flow: beslist welk scherm te tonen, maakt ViewController aan en verbindt ze. Router — abstractie over UINavigationController: push, present, pop, dismiss. Coordinator gebruikt Router om navigatie uit te voeren. In sommige implementaties bevat Router de logica van Coordinator (Router-per-screen), maar dit is een afwijking van het originele patroon.
Twee belangrijke plekken voor lekken: childCoordinators (ouder houdt kind vast, vergeet te verwijderen) en Router (UINavigationController houdt ViewController vast). Oplossing: verwijder altijd de kind-Coordinator uit de array bij finish(). Gebruik weak reference voor delegate. Voor Router — houd geen sterke verwijzing naar UINavigationController als deze al in de window-hiërarchie zit. Test de deinit van Coordinator.
Voor applicaties met 3-5 schermen is Coordinator overbodig — segue of eenvoudige navigationController.pushViewController is eenvoudiger. Voor SwiftUI-applicaties met NavigationStack — ook overbodig. Coordinator is gerechtvaardigd voor applicaties met 15+ schermen, complexe flows (onboarding met vertakkingen, autorisatie met wachtwoordherstel) en mixed UIKit/SwiftUI-projecten.
Mock Router — controleren welke methoden met welke parameters worden aangeroepen. Controle van childCoordinators: na start() is de array niet leeg, na finish() — leeg. Coordinator wordt getest zonder UI: Router is een protocol, de mock vereist geen UIKit. Gebruik XCTestExpectation voor async flow. In Android analoog — testen van NavigationController en NavHost met mock-navigatie.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook