Skill v3.0.0
currentAutomated scan100/1003 files
name: coordinator-pattern description: "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" version: "3.0.0"
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 coordinatorManages childCoordinators arrayDelegates 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 automaticallyNo explicit completion needed
NEVER Do
Child Coordinator Lifecycle
NEVER forget to remove child coordinators:
// ❌ Memory leak — child coordinator retained foreverfinal class ParentCoordinator {var childCoordinators: [Coordinator] = []func startLoginFlow() {let loginCoordinator = LoginCoordinator()childCoordinators.append(loginCoordinator)loginCoordinator.start()// Never removed! Leaks.}}// ✅ Remove child on flow completionfinal class ParentCoordinator: LoginCoordinatorDelegate {var childCoordinators: [Coordinator] = []func startLoginFlow() {let loginCoordinator = LoginCoordinator()loginCoordinator.delegate = selfchildCoordinators.append(loginCoordinator)loginCoordinator.start()}func loginCoordinatorDidFinish(_ coordinator: LoginCoordinator) {childCoordinators.removeAll { $0 === coordinator }}}
NEVER use strong parent references:
// ❌ Retain cycle — coordinator never deallocatesfinal class ChildCoordinator {var parent: ParentCoordinator // Strong reference!}// ✅ Weak parent or delegatefinal class ChildCoordinator {weak var delegate: ChildCoordinatorDelegate?// ORweak var parent: ParentCoordinator?}
Coordinator Responsibilities
NEVER put business logic in coordinators:
// ❌ Coordinator doing business logicfinal class CheckoutCoordinator {func completeOrder() async {// Business logic leaked into coordinator!let total = cart.items.reduce(0) { $0 + $1.price }let tax = total * 0.08try await paymentService.charge(total + tax)}}// ✅ Coordinator orchestrates, ViewModel/UseCase handles logicfinal 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 coordinatorstruct LoginView: View {let coordinator: LoginCoordinatorvar body: some View {Button("Done") {coordinator.parent?.childDidFinish(coordinator) // Wrong!}}}// ✅ View only knows its immediate coordinatorstruct LoginView: View {let coordinator: LoginCoordinatorvar 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 rebuildstruct ParentView: View {var body: some View {ChildView() // Child creates its own coordinator}}struct ChildView: View {@StateObject var coordinator = ChildCoordinator() // Wrong!}// ✅ Parent creates and owns coordinatorstruct ParentView: View {@StateObject var childCoordinator = ChildCoordinator()var body: some View {ChildView(coordinator: childCoordinator)}}
NEVER use NavigationLink directly when using coordinators:
// ❌ Bypasses coordinator — navigation untrackedstruct UserListView: View {var body: some View {NavigationLink("User") {UserDetailView() // Coordinator doesn't know about this!}}}// ✅ Delegate navigation to coordinatorstruct UserListView: View {@ObservedObject var coordinator: UsersCoordinatorvar body: some View {Button("User") {coordinator.showUserDetail(userId: "123")}}}
Essential Patterns
SwiftUI Coordinator Protocol
@MainActorprotocol Coordinator: ObservableObject {associatedtype Route: Hashablevar path: NavigationPath { get set }func start() -> AnyViewfunc 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
@MainActorprotocol 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@MainActorfinal class TabCoordinator: ParentCoordinatorProtocol, ObservableObject {var childCoordinators: [any Coordinator] = []lazy var homeCoordinator: HomeCoordinator = {let coordinator = HomeCoordinator()coordinator.parent = selfaddChild(coordinator)return coordinator}()lazy var profileCoordinator: ProfileCoordinator = {let coordinator = ProfileCoordinator()coordinator.parent = selfaddChild(coordinator)return coordinator}()}
Flow Completion with Result
protocol LoginCoordinatorDelegate: AnyObject {func loginCoordinator(_ coordinator: LoginCoordinator, didFinishWith result: LoginResult)}enum LoginResult {case success(User)case cancelled}@MainActorfinal class LoginCoordinator: ObservableObject {weak var delegate: LoginCoordinatorDelegate?@Published var path = NavigationPath()enum Route: Hashable {case credentialscase forgotPasswordcase twoFactor(email: String)}func completeLogin(user: User) {delegate?.loginCoordinator(self, didFinishWith: .success(user))}func cancel() {delegate?.loginCoordinator(self, didFinishWith: .cancelled)}}
Deep Link Integration
@MainActorfinal class AppCoordinator: ObservableObject, ParentCoordinatorProtocol {var childCoordinators: [any Coordinator] = []@Published var path = NavigationPath()func handle(deepLink: DeepLink) {// Reset to known statepopToRoot()childCoordinators.forEach { removeChild($0) }// Navigate to deep link destinationswitch deepLink {case .user(let id):navigate(to: .userList)navigate(to: .userDetail(userId: id))case .checkout:let checkoutCoordinator = CheckoutCoordinator()checkoutCoordinator.delegate = selfaddChild(checkoutCoordinator)// Present checkout flowcase .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 |