SwiftQA Random

What are coordinators?

Coordinators are a navigation pattern — they pull the responsibility of “which screen comes next” out of view controllers/views and into dedicated objects whose only job is managing flow.

The problem they solve

In plain MVC or even MVVM, navigation logic tends to leak into view controllers: prepareForSegue, navigationController?.pushViewController(...), deciding which screen to show next based on some business condition. This causes two specific problems:

  1. View controllers become coupled to each other — Screen A has to know Screen B exists in order to instantiate and push it. That makes each screen hard to reuse or test in isolation, and hard to reorder in a flow.
  2. Navigation logic is untestable — it’s buried inside UIViewController lifecycle methods, so you can’t unit test “does this flow branch to screen X when condition Y is true” without instantiating a whole view controller hierarchy.

The pattern

A Coordinator is a plain object (not a view controller) that owns:

  • A reference to a navigation controller (or similar)
  • The logic for “what screen comes next”
  • Often, child coordinators for sub-flows
protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }
    func start()
}

final class OnboardingCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController

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

    func start() {
        let vc = WelcomeViewController()
        vc.onContinue = { [weak self] in
            self?.showSignUp()
        }
        navigationController.pushViewController(vc, animated: false)
    }

    func showSignUp() {
        let vc = SignUpViewController()
        vc.onComplete = { [weak self] in
            self?.finishOnboarding()
        }
        navigationController.pushViewController(vc, animated: true)
    }

    func finishOnboarding() {
        // delegate up to parent coordinator, e.g. AppCoordinator,
        // to switch to the main app flow
    }
}

Each view controller doesn’t know what comes next — it just exposes a closure or delegate (“user tapped continue”), and the coordinator decides where that leads. Screen A no longer imports or instantiates Screen B directly.

Typical hierarchy

An AppCoordinator sits at the root, owns child coordinators for major flows (OnboardingCoordinator, AuthCoordinator, MainTabCoordinator), and each of those can own further children. This tree structure is what makes complex, branching, or conditional flows manageable.

Why it pairs naturally with MVVM

MVVM already pulls business/presentation logic out of the view into the ViewModel; Coordinators pull the remaining piece — navigation — out too. ViewModel handles “what should this screen display and do,” Coordinator handles “what screen comes after this one.”

Tradeoffs, honestly

Good for:

  • Testing navigation logic independent of UI
  • Reusing screens across different flows
  • Deep linking
  • Decoupling screens from each other

Bad for / costs:

  • Boilerplate — every flow needs a coordinator class, protocol conformance, closure/delegate wiring
  • Memory management gotchas — child coordinator arrays need careful cleanup or you leak coordinators and their retained view controllers
  • Passing data between screens gets more indirect
  • In SwiftUI, the pattern is less commonly used verbatim since NavigationStack + path-based navigation often achieves the same decoupling more natively

Interview-ready one-liner: “Coordinators exist to answer ‘who decides what screen comes next’ without making screens know about each other — it’s specifically solving navigation coupling and untestable flow logic, not general business logic, which is what ViewModels are for.”