coordinator-pattern

Coordinator Pattern — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "coordinator-pattern" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-coordinator-pattern

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review