Coordinator: kluczowe pojęcia, wzorzec koordynatora dla nawigacji iOS

Autor: IT Sectr Opublikowano: 2026-02-18 Czas czytania: 9 min

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 nawigacji, przenoszący logikę przejść z ViewController
  • Podział odpowiedzialności — ViewController zarządza UI, Coordinator — nawigacją
  • Router — komponent pomocniczy Coordinatora do abstrakcji UINavigationController
  • Flow — sekwencja ekranów zarządzana przez jeden Coordinator (np. onboarding)
  • Delegacja — dziecięce koordynatory raportują rodzicowi przez delegate/protocol

Czym jest Coordinator: istota wzorca nawigacji

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.

KomponentRolaPrzykład
CoordinatorZarządzanie flow nawigacjiAuthCoordinator, ProfileCoordinator
RouterAbstrakcja nad UINavigationControllerpush, present, pop, dismiss
ViewControllerUI + delegacja zdarzeń do CoordinatorLoginViewController.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.

Coordinator w Swift: implementacja z Router i Flow

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.

swift
// 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.

Dziecięce Coordinator i hierarchia koordynatorów

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.

swift
// 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.

Sposoby przekazywania danych między koordynatorami

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, gdzie Output to typ generyczny wyniku flow. Coordinator — generyk z typem stowarzyszonym wyniku. start() z callback: start(completion: @escaping (Output) -> Void). To skraca kod: nie trzeba pisać osobnego protokołu dla każdego koordynatora. RxSwift/Combine: Coordinator publikuje wynik przez PassthroughSubject/Publisher. Wybór zależy od podejścia architektonicznego zespołu.

MetodaZaletyWady
DelegateTypowo bezpieczne, jawne, osobne protokołyDużo protokołów, dużo boilerplate'u
ClosureKompaktowe, mniej plikówTrudno debugować retain cycle
Combine/RxReaktywne, ł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.

Porównanie Coordinator z Router, VIPER i MVVM-C

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.

swift
// 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

Czy Coordinator jest potrzebny dla SwiftUI?

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.

Czy Coordinator to Router?

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.

Jak uniknąć retain cycle w Coordinator?

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.

Kiedy Coordinator jest zbędny?

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.

Jak testować Coordinator?

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

  • Coordinator — wzorzec nawigacji, przenoszący przejścia z ViewController do oddzielnej klasy
  • Router — abstrakcja UINavigationController używana przez Coordinatora
  • Hierarchia — rodzicielskie i dziecięce Coordinator z delegowaniem wyników
  • MVVM-C — standardowe podejście dla projektów UIKit z Coordinator
  • SwiftUI — wbudowana nawigacja przez NavigationStack zastępuje Coordinator
  • Testowanie — Coordinator testowany przez mock Router bez UIKit

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.

Omów projekt

Przeczytaj również