Skip to content

Coordinator ​

Coordinator is an iOS architecture pattern for keeping navigation decisions outside individual screens. A coordinator creates screens, responds to their navigation events, and owns the flow's UINavigationController.

When it helps in iOS ​

An onboarding flow may show a welcome screen, permissions screen, and account setup. If each controller creates the next one, navigation decisions spread across the screens. A coordinator gives the flow one owner.

Example: a two-step onboarding flow ​

swift
import UIKit

final class WelcomeViewController: UIViewController {
    var onContinue: (() -> Void)?

    // Call onContinue?() from the Continue button action.
}

final class PermissionsViewController: UIViewController {}

final class OnboardingCoordinator {
    private let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let welcome = WelcomeViewController()
        welcome.onContinue = { [weak self] in
            self?.showPermissions()
        }
        navigationController.setViewControllers([welcome], animated: false)
    }

    private func showPermissions() {
        let permissions = PermissionsViewController()
        navigationController.pushViewController(permissions, animated: true)
    }
}

// Keep the coordinator alive for as long as its flow is active.

The welcome screen reports an intent; the coordinator chooses the destination. The weak capture avoids a retain cycle between the view controller's closure and the coordinator.

How to use it in a real app ​

The scene or app coordinator creates and retains OnboardingCoordinator, then calls start(). If the flow creates child coordinators, retain each child until it finishes and remove it on completion. Pass services into screens during construction rather than letting screens reach into the coordinator for business data.

Watch for: In a small SwiftUI flow, a NavigationStack with route state may be enough. A coordinator is most useful when navigation spans multiple UIKit controllers, deep links, or nested flows.

Trace ownership ​

The app or scene owns the coordinator. The coordinator owns the navigation controller reference and creates the welcome screen. The screen reports a Continue event through its closure; the coordinator decides to push permissions. Because the closure is stored on the screen, it captures the coordinator weakly.

A real onboarding flow should finish by notifying its parent coordinator and releasing its child coordinator. Test navigation by injecting a navigation abstraction or inspecting the pushed controllers. Keep feature data flow in view models and services; a coordinator should make routing decisions, not become a second view model.

Identify the owner of each decision ​

The app or scene retains OnboardingCoordinator; the coordinator receives a navigation controller and creates the first screen. WelcomeViewController does not know which screen comes next. It only reports Continue through onContinue. The coordinator interprets that event and pushes PermissionsViewController. The weak capture prevents the stored callback from retaining the coordinator through the screen it created.

From example to a full iOS flow ​

A parent app coordinator can choose whether to start onboarding or the signed-in app based on session state. When onboarding ends, the child coordinator should report completion, let the parent replace the root navigation stack, and then be released. Deep links that arrive during onboarding need a policy: queue the destination, reject it, or route to sign-in first. Keep those routing decisions in coordinators while form validation stays in view models or services.

Failure modes and verification ​

If no owner retains the coordinator, it deallocates before the Continue callback. If child coordinators are never removed, entire navigation flows leak. Test the sequence of pushed controllers and coordinator release after completion. SwiftUI's NavigationStack with an explicit route enum can solve small flows with less machinery; UIKit flows with multiple controllers and deep links benefit more from a coordinator.

Back to all Swift Design Patterns

Released under the MIT License.