Claude-skill-registry coordinator-pattern
Expert Coordinator pattern decisions for iOS/tvOS: when coordinators add value vs overkill, parent-child coordinator hierarchy design, SwiftUI vs UIKit coordinator differences, and flow completion handling. Use when designing navigation architecture, implementing multi-step flows, or decoupling views from navigation. Trigger keywords: Coordinator, navigation, flow, parent coordinator, child coordinator, deep link, routing, navigation hierarchy, flow completion
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/coordinator-pattern" ~/.claude/skills/majiayu000-claude-skill-registry-coordinator-pattern && rm -rf "$T"
skills/data/coordinator-pattern/SKILL.mdCoordinator 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 |