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
Repository
kaakati/rails-e…rise-devGitHub Stars
6
First Seen
Jan 25, 2026
Security Audits
Installed on
claude-code13
opencode13
gemini-cli12
codex12
antigravity11
github-copilot11