skills/kaakati/rails-enterprise-dev/coordinator-pattern

coordinator-pattern

SKILL.md

Coordinator Pattern — Expert Decisions

Expert decision frameworks for Coordinator pattern choices. Claude knows the pattern — this skill provides judgment calls for when coordinators add value and how to structure hierarchies.


Decision Trees

Do You Need Coordinators?

How many navigation flows does your app have?
├─ 1-2 simple flows
│  └─ Skip coordinators
│     NavigationStack + simple Router is enough
├─ 3-5 distinct flows
│  └─ Consider coordinators IF:
│     • Flows have complex branching
│     • Deep linking is required
│     • Flows need to share navigation logic
└─ 6+ flows or multi-team development
   └─ Coordinators recommended
      • Clear ownership boundaries
      • Parallel development possible
      • Testable navigation logic

The trap: Adding coordinators to simple apps. If your app is 5 screens with linear flow, coordinators add complexity without benefit.

Coordinator Hierarchy Design

Does this flow need to manage sub-flows?
├─ NO (leaf coordinator)
│  └─ Simple coordinator
│     Owns NavigationPath, creates views
└─ YES (has sub-flows)
   └─ Parent coordinator
      Manages childCoordinators array
      Delegates to child for sub-flows

SwiftUI vs UIKit Coordinator

Which UI framework?
├─ SwiftUI
│  └─ Coordinator as ObservableObject
│     • Owns NavigationPath
│     • @ViewBuilder for destinations
│     • Pass via EnvironmentObject or explicit injection
└─ UIKit
   └─ Coordinator owns UINavigationController
      • Creates and pushes ViewControllers
      • Uses delegation for flow completion
      • Manages childCoordinators manually

Flow Completion Strategy

How does a flow end?
├─ Success (user completed task)
│  └─ Delegate method with result
│     coordinator.didCompleteLogin(user: user)
├─ Cancellation (user backed out)
│  └─ Delegate method without result
│     coordinator.didCancelLogin()
└─ Automatic (flow naturally ends)
   └─ Parent removes child automatically
      No explicit completion needed

NEVER Do

Child Coordinator Lifecycle

NEVER forget to remove child coordinators:

// ❌ Memory leak — child coordinator retained forever
final class ParentCoordinator {
    var childCoordinators: [Coordinator] = []

    func startLoginFlow() {
        let loginCoordinator = LoginCoordinator()
        childCoordinators.append(loginCoordinator)
        loginCoordinator.start()
        // Never removed! Leaks.
    }
}

// ✅ Remove child on flow completion
final class ParentCoordinator: LoginCoordinatorDelegate {
    var childCoordinators: [Coordinator] = []

    func startLoginFlow() {
        let loginCoordinator = LoginCoordinator()
        loginCoordinator.delegate = self
        childCoordinators.append(loginCoordinator)
        loginCoordinator.start()
    }

    func loginCoordinatorDidFinish(_ coordinator: LoginCoordinator) {
        childCoordinators.removeAll { $0 === coordinator }
    }
}

NEVER use strong parent references:

// ❌ Retain cycle — coordinator never deallocates
final class ChildCoordinator {
    var parent: ParentCoordinator  // Strong reference!
}

// ✅ Weak parent or delegate
final class ChildCoordinator {
    weak var delegate: ChildCoordinatorDelegate?
    // OR
    weak var parent: ParentCoordinator?
}

Coordinator Responsibilities

NEVER put business logic in coordinators:

// ❌ Coordinator doing business logic
final class CheckoutCoordinator {
    func completeOrder() async {
        // Business logic leaked into coordinator!
        let total = cart.items.reduce(0) { $0 + $1.price }
        let tax = total * 0.08
        try await paymentService.charge(total + tax)
    }
}

// ✅ Coordinator orchestrates, ViewModel/UseCase handles logic
final class CheckoutCoordinator {
    func showCheckout() {
        let viewModel = CheckoutViewModel(
            cartService: container.cartService,
            paymentService: container.paymentService
        )
        // ViewModel handles business logic
    }
}

NEVER let views know about coordinator hierarchy:

// ❌ View knows about parent coordinator
struct LoginView: View {
    let coordinator: LoginCoordinator

    var body: some View {
        Button("Done") {
            coordinator.parent?.childDidFinish(coordinator)  // Wrong!
        }
    }
}

// ✅ View only knows its immediate coordinator
struct LoginView: View {
    let coordinator: LoginCoordinator

    var body: some View {
        Button("Done") {
            coordinator.completeLogin()  // Coordinator handles delegation
        }
    }
}

SwiftUI-Specific

NEVER create coordinators as @StateObject in child views:

// ❌ New coordinator created on every parent rebuild
struct ParentView: View {
    var body: some View {
        ChildView()  // Child creates its own coordinator
    }
}

struct ChildView: View {
    @StateObject var coordinator = ChildCoordinator()  // Wrong!
}

// ✅ Parent creates and owns coordinator
struct ParentView: View {
    @StateObject var childCoordinator = ChildCoordinator()

    var body: some View {
        ChildView(coordinator: childCoordinator)
    }
}

NEVER use NavigationLink directly when using coordinators:

// ❌ Bypasses coordinator — navigation untracked
struct UserListView: View {
    var body: some View {
        NavigationLink("User") {
            UserDetailView()  // Coordinator doesn't know about this!
        }
    }
}

// ✅ Delegate navigation to coordinator
struct UserListView: View {
    @ObservedObject var coordinator: UsersCoordinator

    var body: some View {
        Button("User") {
            coordinator.showUserDetail(userId: "123")
        }
    }
}

Essential Patterns

SwiftUI Coordinator Protocol

@MainActor
protocol Coordinator: ObservableObject {
    associatedtype Route: Hashable
    var path: NavigationPath { get set }

    func start() -> AnyView
    func navigate(to route: Route)
    func pop()
    func popToRoot()
}

extension Coordinator {
    func pop() {
        guard !path.isEmpty else { return }
        path.removeLast()
    }

    func popToRoot() {
        path = NavigationPath()
    }
}

Parent-Child Coordinator

@MainActor
protocol ParentCoordinatorProtocol: AnyObject {
    var childCoordinators: [any Coordinator] { get set }
    func addChild(_ coordinator: any Coordinator)
    func removeChild(_ coordinator: any Coordinator)
}

extension ParentCoordinatorProtocol {
    func addChild(_ coordinator: any Coordinator) {
        childCoordinators.append(coordinator)
    }

    func removeChild(_ coordinator: any Coordinator) {
        childCoordinators.removeAll { $0 === coordinator as AnyObject }
    }
}

// Tab coordinator managing child coordinators
@MainActor
final class TabCoordinator: ParentCoordinatorProtocol, ObservableObject {
    var childCoordinators: [any Coordinator] = []

    lazy var homeCoordinator: HomeCoordinator = {
        let coordinator = HomeCoordinator()
        coordinator.parent = self
        addChild(coordinator)
        return coordinator
    }()

    lazy var profileCoordinator: ProfileCoordinator = {
        let coordinator = ProfileCoordinator()
        coordinator.parent = self
        addChild(coordinator)
        return coordinator
    }()
}

Flow Completion with Result

protocol LoginCoordinatorDelegate: AnyObject {
    func loginCoordinator(_ coordinator: LoginCoordinator, didFinishWith result: LoginResult)
}

enum LoginResult {
    case success(User)
    case cancelled
}

@MainActor
final class LoginCoordinator: ObservableObject {
    weak var delegate: LoginCoordinatorDelegate?
    @Published var path = NavigationPath()

    enum Route: Hashable {
        case credentials
        case forgotPassword
        case twoFactor(email: String)
    }

    func completeLogin(user: User) {
        delegate?.loginCoordinator(self, didFinishWith: .success(user))
    }

    func cancel() {
        delegate?.loginCoordinator(self, didFinishWith: .cancelled)
    }
}

Deep Link Integration

@MainActor
final class AppCoordinator: ObservableObject, ParentCoordinatorProtocol {
    var childCoordinators: [any Coordinator] = []
    @Published var path = NavigationPath()

    func handle(deepLink: DeepLink) {
        // Reset to known state
        popToRoot()
        childCoordinators.forEach { removeChild($0) }

        // Navigate to deep link destination
        switch deepLink {
        case .user(let id):
            navigate(to: .userList)
            navigate(to: .userDetail(userId: id))

        case .checkout:
            let checkoutCoordinator = CheckoutCoordinator()
            checkoutCoordinator.delegate = self
            addChild(checkoutCoordinator)
            // Present checkout flow

        case .settings(let section):
            navigate(to: .settings)
            if let section = section {
                navigate(to: .settingsSection(section))
            }
        }
    }
}

Quick Reference

Coordinator Checklist

  • Coordinator owns NavigationPath (SwiftUI) or UINavigationController (UIKit)
  • Parent-child references are weak
  • Child coordinators removed on flow completion
  • Views don't know about coordinator hierarchy
  • Business logic stays in ViewModels/UseCases
  • Deep links handled at appropriate coordinator level

When to Use Coordinators

Scenario Use Coordinator?
Simple 3-5 screen app No — simple Router
Multiple independent flows Yes
Deep linking required Likely yes
Multi-step wizard flows Yes
Cross-tab navigation Yes
A/B testing navigation Yes
Team-based feature ownership Yes

Red Flags

Smell Problem Fix
childCoordinators grows forever Memory leak Remove on completion
Strong parent reference Retain cycle Use weak or delegate
Business logic in coordinator Wrong layer Move to ViewModel/UseCase
View creates NavigationLink Bypasses coordinator Delegate to coordinator
@StateObject coordinator in child Recreated on rebuild Parent owns coordinator
Coordinator creates its own views Can't inject dependencies Use ViewFactory

Coordinator vs Router

Aspect Coordinator Router
Complexity Higher Lower
Hierarchy support Yes (parent-child) No
Flow isolation Strong Weak
Testing Excellent Good
Learning curve Steep Gentle
Best for Large apps, teams Small-medium apps
Weekly Installs
15
GitHub Stars
6
First Seen
Jan 25, 2026
Installed on
claude-code13
opencode13
gemini-cli12
codex12
antigravity11
github-copilot11