Skip to content
advanced Phase 9 · State Management & Architecture Patterns

Coordinator Pattern

Manage complex navigation flows with coordinators for decoupled, testable navigation.

55m
3 problems
Topic Progress 0%

Coordinator Architecture

Why Coordinators?

The Coordinator pattern centralizes navigation logic outside of view controllers and views. It provides a single source of truth for navigation flows, making apps easier to test and maintain.

Coordinator Protocol

Define a base protocol that all coordinators conform to.

protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    func start()
}

extension Coordinator {
    func addChild(_ coordinator: Coordinator) {
        childCoordinators.append(coordinator)
    }
    
    func removeChild(_ coordinator: Coordinator) {
        childCoordinators = childCoordinators.filter { $0 !== coordinator }
    }
    
    func removeAllChildren() {
        childCoordinators.removeAll()
    }
}

App Coordinator

The root coordinator manages the main navigation and child coordinators.

class AppCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        showLogin()
    }
    
    func showLogin() {
        let loginCoordinator = LoginCoordinator()
        loginCoordinator.delegate = self
        addChild(loginCoordinator)
        loginCoordinator.start()
    }
    
    func showMainApp() {
        let mainCoordinator = MainTabCoordinator()
        addChild(mainCoordinator)
        mainCoordinator.start()
    }
}

extension AppCoordinator: LoginCoordinatorDelegate {
    func loginDidComplete() {
        removeAllChildren()
        showMainApp()
    }
}

Feature Coordinators

Each feature has its own coordinator managing its navigation flow with delegate callbacks to report completion.

Child Coordinators

Managing Child Coordinators

Child coordinators handle specific navigation flows within a feature. They report completion back to the parent via delegate pattern.

class ProfileCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    weak var delegate: ProfileCoordinatorDelegate?
    
    func start() {
        let profileVC = ProfileViewController()
        profileVC.onEditProfile = { [weak self] in self?.showEditProfile() }
        profileVC.onLogout = { [weak self] in self?.delegate?.profileDidLogout() }
    }
    
    func showEditProfile() {
        let editCoordinator = EditProfileCoordinator()
        editCoordinator.delegate = self
        addChild(editCoordinator)
        editCoordinator.start()
    }
}

extension ProfileCoordinator: EditProfileCoordinatorDelegate {
    func editDidComplete() { removeChild(self) }
}

Coordinator as ObservableObject

For SwiftUI, make coordinators ObservableObject to drive view state.

class ShopCoordinator: ObservableObject, Coordinator {
    var childCoordinators: [Coordinator] = []
    @Published var currentView: ShopView = .list
    
    enum ShopView: Hashable {
        case list, cart, checkout
    }
    
    func start() { currentView = .list }
    func showCart() { currentView = .cart }
    func showCheckout() { currentView = .checkout }
}

Using Coordinators in SwiftUI

Inject coordinators into the SwiftUI environment for navigation.

struct ShopView: View {
    @StateObject var coordinator = ShopCoordinator()
    
    var body: some View {
        NavigationStack {
            List {
                Button("View Cart") { coordinator.showCart() }
            }
            .navigationDestination(for: ShopCoordinator.ShopView.self) { view in
                switch view {
                case .cart: CartView()
                case .checkout: CheckoutView()
                case .list: EmptyView()
                }
            }
        }
        .environmentObject(coordinator)
    }
}

Coordinator in SwiftUI

NavigationPath with Coordinators

Combine NavigationPath with coordinators for type-safe navigation.

class NavigationCoordinator: ObservableObject {
    var path = NavigationPath()
    
    func showDetail(id: String) {
        path.append(id)
    }
    
    func showSettings() {
        path.append(SettingsRoute())
    }
    
    func popToRoot() {
        path = NavigationPath()
    }
}

struct ContentView: View {
    @StateObject var coordinator = NavigationCoordinator()
    
    var body: some View {
        NavigationStack(path: $coordinator.path) {
            HomeView()
                .navigationDestination(for: String.self) { id in
                    DetailView(id: id)
                }
                .navigationDestination(for: SettingsRoute.self) { _ in
                    SettingsView()
                }
        }
        .environmentObject(coordinator)
    }
}

Sheet and FullScreenCover Coordination

Handle modal presentations through coordinators.

enum SheetType: Identifiable {
    case profile, settings, about
    var id: String { String(describing: self) }
}

class SheetCoordinator: ObservableObject {
    @Published var activeSheet: SheetType?
    @Published var activeFullScreenCover: SheetType?
    
    func showProfile() { activeSheet = .profile }
    func showSettings() { activeFullScreenCover = .settings }
    func dismiss() { activeSheet = nil; activeFullScreenCover = nil }
}

Coordinator Testing

Coordinators make navigation logic testable by mocking delegate callbacks.

class MockLoginCoordinatorDelegate: LoginCoordinatorDelegate {
    var loginCompleted = false
    func loginDidComplete() { loginCompleted = true }
}

func testLoginFlow() {
    let coordinator = LoginCoordinator()
    let delegate = MockLoginCoordinatorDelegate()
    coordinator.delegate = delegate
    coordinator.start()
    coordinator.loginButtonTapped()
    assert(delegate.loginCompleted)
}

Quiz

1. What is the main benefit of the Coordinator pattern?

Question 1 options

2. How do child coordinators communicate completion to parents?

Question 2 options

3. How do you integrate coordinators with SwiftUI NavigationStack?

Question 3 options

4. What does the start() method do in a coordinator?

Question 4 options

Flashcards

Question

What is the Coordinator pattern?

Answer

A pattern that centralizes navigation logic in dedicated coordinator objects, separating it from views and view controllers.

Question

What is the role of an App Coordinator?

Answer

The root coordinator that manages the main navigation structure and creates child coordinators for features.

Question

How do coordinators report navigation completion?

Answer

Through delegate protocols - child coordinators call delegate methods when their flow completes.

Question

How do coordinators work with SwiftUI?

Answer

Make coordinators ObservableObject and use NavigationPath for type-safe navigation binding.

Revision Notes

Key Takeaways

  • 1. Coordinators centralize navigation logic outside views
  • 2. Child coordinators report completion via delegates
  • 3. ObservableObject makes coordinators work with SwiftUI
  • 4. NavigationPath provides type-safe navigation
  • 5. Coordinators improve testability of navigation flows

Interview Tips

  • Explain why coordinators are better than navigation in view controllers
  • Describe how to handle complex navigation flows with child coordinators
  • Discuss how coordinators make navigation testable

Cheat Sheet

Coordinator Pattern Quick Reference

  • Coordinator protocol - Base protocol with start() and childCoordinators
  • AppCoordinator - Root coordinator for main navigation
  • Child Coordinators - Feature-specific navigation logic
  • Delegate pattern - Communication from child to parent
  • NavigationPath - Type-safe SwiftUI navigation
  • ObservableObject - SwiftUI integration
  • start() - Initialize navigation flow
  • removeAllChildren() - Clean up child coordinators