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 |
More from kaakati/rails-enterprise-dev
flutter conventions & best practices
Dart 3.x and Flutter 3.x conventions, naming patterns, code organization, null safety, and async/await best practices
55getx state management patterns
GetX controllers, reactive state, dependency injection, bindings, navigation, and best practices
52tailadmin ui patterns
TailAdmin dashboard UI framework patterns and Tailwind CSS classes. ALWAYS use this skill when: (1) Building any dashboard or admin panel interface, (2) Creating data tables, cards, charts, or metrics displays, (3) Implementing forms, buttons, alerts, or modals, (4) Building navigation (sidebar, header, breadcrumbs), (5) Any UI work that should follow TailAdmin design. This skill REQUIRES fetching from the official GitHub repository to ensure accurate class usage - NEVER invent classes.
39mvvm-architecture
Expert MVVM decisions for iOS/tvOS: choosing between ViewModel patterns (state enum vs published properties vs Combine), service layer boundaries, dependency injection strategies, and testing approaches. Use when designing ViewModel architecture, debugging data flow issues, or deciding where business logic belongs. Trigger keywords: MVVM, ViewModel, ObservableObject, @StateObject, service layer, dependency injection, unit test, mock, architecture
36rails localization (i18n) - english & arabic
Comprehensive internationalization skill for Ruby on Rails applications with proper English and Arabic translations, RTL support, pluralization rules, date/time formatting, and culturally appropriate content adaptation.
34rspec testing patterns
Complete guide to testing Ruby on Rails applications with RSpec. Use this skill when writing unit tests, integration tests, system tests, or when setting up test infrastructure including factories, shared examples, and mocking strategies.
31