Bird
Raised Fist0
iOS Swiftmobile~15 mins

Coordinator pattern in iOS Swift - Deep Dive

Choose your learning style10 modes available
Overview - Coordinator pattern
What is it?
The Coordinator pattern is a way to organize navigation and flow in an iOS app. It moves the responsibility of moving between screens out of view controllers and into separate coordinator objects. This helps keep the app's structure clean and easier to manage, especially as it grows.
Why it matters
Without the Coordinator pattern, view controllers get overloaded with navigation code, making them hard to read and maintain. This can cause bugs and slow down development. Using coordinators makes the app easier to understand, test, and change, improving the user experience and developer productivity.
Where it fits
Before learning this, you should understand basic iOS app structure, view controllers, and navigation controllers. After this, you can explore advanced app architecture patterns like MVVM or Combine integration with coordinators.
Mental Model
Core Idea
A coordinator is like a traffic controller that directs which screen to show next, keeping view controllers focused on their own content.
Think of it like...
Imagine a theater play where actors (view controllers) perform their roles but a stage manager (coordinator) decides when each actor comes on stage and where they move. The actors don’t worry about the timing or order, just their performance.
┌───────────────┐      ┌───────────────┐
│  Coordinator  │─────▶│ ViewController│
│ (Traffic Ctrl)│      │  (Actor)      │
└───────────────┘      └───────────────┘
       ▲                      ▲
       │                      │
       └───── Manages flow ───┘
Build-Up - 6 Steps
1
FoundationWhat is a Coordinator?
🤔
Concept: Introduces the coordinator as a separate object that handles navigation.
In iOS, navigation is often done inside view controllers, but this mixes UI and flow logic. A coordinator is a new object that takes over navigation tasks. It creates view controllers and decides which one to show next.
Result
You get a cleaner separation: view controllers focus on UI, coordinators focus on navigation.
Understanding that navigation can be separated from UI code helps keep each part simpler and easier to manage.
2
FoundationBasic Coordinator Structure
🤔
Concept: Shows the minimal code structure of a coordinator in Swift.
A coordinator usually has a start() method to launch its flow. It holds a reference to a navigation controller to push or present view controllers. Example: class Coordinator { let navigationController: UINavigationController init(navigationController: UINavigationController) { self.navigationController = navigationController } func start() { // Show first screen } }
Result
You can create a coordinator instance and call start() to begin navigation.
Knowing the coordinator owns navigationController clarifies how it controls screen transitions.
3
IntermediateHandling Multiple Flows
🤔Before reading on: do you think one coordinator can handle all app navigation or should there be many? Commit to your answer.
Concept: Explains splitting navigation into multiple coordinators for different app parts.
Large apps have many flows (login, main app, settings). Each flow can have its own coordinator. A parent coordinator manages child coordinators. This keeps each flow isolated and easier to maintain.
Result
Navigation is modular: you can add or remove flows without changing unrelated parts.
Understanding modular coordinators prevents tangled navigation code and supports app scalability.
4
IntermediateCommunication Between Coordinators
🤔Before reading on: do you think child coordinators directly change parent navigation or notify the parent? Commit to your answer.
Concept: Shows how coordinators communicate using delegation or callbacks instead of direct navigation changes.
Child coordinators inform parents when they finish or need to trigger another flow. Parents then decide what to do next. This avoids tight coupling and keeps navigation decisions centralized.
Result
Coordinators stay loosely connected, making the app easier to test and change.
Knowing to use delegation for communication avoids bugs from unexpected navigation changes.
5
AdvancedMemory Management in Coordinators
🤔Before reading on: do you think coordinators can cause memory leaks if not handled properly? Commit to your answer.
Concept: Discusses how strong references between coordinators and view controllers can cause leaks and how to avoid them.
Coordinators often hold strong references to navigation controllers and child coordinators. If not released, this causes memory leaks. Use weak references for delegates and remove child coordinators when done.
Result
The app uses memory efficiently and avoids crashes or slowdowns.
Understanding reference cycles in coordinators helps prevent hard-to-find bugs in real apps.
6
ExpertAdvanced Coordinator Patterns and Variations
🤔Before reading on: do you think all coordinators must use UINavigationController or can they work with other UI patterns? Commit to your answer.
Concept: Explores variations like using coordinators with SwiftUI, tab bars, or modal flows, and combining with other architectures.
Coordinators can manage any navigation style, not just push/pop. They can coordinate tab bar controllers, modals, or SwiftUI navigation stacks. They also integrate with MVVM or Combine for reactive flows.
Result
You can adapt the coordinator pattern to many app designs and technologies.
Knowing the pattern’s flexibility allows you to apply it beyond UIKit and build modern, maintainable apps.
Under the Hood
Coordinators work by owning navigation controllers and creating view controllers programmatically. They keep references to child coordinators to manage flow lifecycles. Communication uses delegation or closures to avoid tight coupling. Memory management relies on weak references to prevent retain cycles.
Why designed this way?
The pattern was created to solve the problem of massive view controllers overloaded with navigation code. Separating navigation into coordinators improves code clarity and testability. Alternatives like putting navigation in app delegates or view controllers were too messy or inflexible.
┌───────────────┐       ┌─────────────────────┐
│ Parent Coord  │──────▶│ Child Coord (Login) │
│ (Manages flow)│       └─────────────────────┘
│               │
│  ┌───────────────┐  │       ┌─────────────────────┐
│  │ UINavigationController │
│  └───────────────┘  │──────▶│ ViewController (Login)│
└───────────────┘       └─────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Do coordinators replace view controllers entirely? Commit to yes or no.
Common Belief:Coordinators replace view controllers and handle UI too.
Tap to reveal reality
Reality:Coordinators only handle navigation and flow, not UI or business logic inside view controllers.
Why it matters:Mixing UI code into coordinators defeats their purpose and leads to messy code.
Quick: Is it okay for coordinators to hold strong references to their child coordinators forever? Commit to yes or no.
Common Belief:Keeping strong references to child coordinators forever is fine.
Tap to reveal reality
Reality:Child coordinators must be removed when done to avoid memory leaks.
Why it matters:Not cleaning up coordinators causes memory bloat and app crashes.
Quick: Can a single coordinator handle all navigation in a large app easily? Commit to yes or no.
Common Belief:One coordinator can manage all navigation in any app size.
Tap to reveal reality
Reality:Large apps need multiple coordinators to keep navigation modular and manageable.
Why it matters:Using one coordinator for everything leads to complex, hard-to-maintain code.
Quick: Do coordinators always require UINavigationController? Commit to yes or no.
Common Belief:Coordinators must use UINavigationController to work.
Tap to reveal reality
Reality:Coordinators can manage any navigation style, including modals, tab bars, or SwiftUI navigation.
Why it matters:Believing this limits the pattern’s flexibility and modern usage.
Expert Zone
1
Coordinators often implement a 'finish' callback to notify parents when their flow ends, enabling clean removal and transition.
2
Using protocols for coordinator interfaces allows swapping implementations easily, supporting testing and modularity.
3
Combining coordinators with reactive frameworks like Combine or RxSwift can simplify asynchronous navigation flows.
When NOT to use
Avoid using coordinators for very simple apps with only one or two screens where adding them adds unnecessary complexity. Instead, use direct navigation in view controllers. Also, if your app heavily relies on declarative UI frameworks like SwiftUI, consider using native navigation tools before adding coordinators.
Production Patterns
In real apps, coordinators are often combined with MVVM architecture, where view models handle data and coordinators handle navigation. Parent coordinators manage child coordinators for features like onboarding, authentication, and main app flows. Coordinators also handle deep linking by interpreting URLs and starting the correct flow.
Connections
Model-View-ViewModel (MVVM)
Builds-on
Understanding coordinators helps separate navigation from UI and business logic, which MVVM also aims to separate, making the app more modular and testable.
Event-driven Architecture
Same pattern
Coordinators use delegation and callbacks to communicate, similar to event-driven systems where components react to events without tight coupling.
Air Traffic Control Systems
Analogy-based inspiration
Just like air traffic controllers manage plane movements safely and efficiently, coordinators manage screen transitions to keep app flow smooth and organized.
Common Pitfalls
#1Putting navigation code inside view controllers instead of coordinators.
Wrong approach:class ViewController: UIViewController { func goToNext() { let nextVC = NextViewController() navigationController?.pushViewController(nextVC, animated: true) } }
Correct approach:class Coordinator { let navigationController: UINavigationController func start() { let vc = ViewController() vc.onNext = { [weak self] in self?.showNext() } navigationController.pushViewController(vc, animated: true) } func showNext() { let nextVC = NextViewController() navigationController.pushViewController(nextVC, animated: true) } }
Root cause:Confusing navigation responsibility leads to bloated view controllers and tangled code.
#2Not removing child coordinators after their flow finishes.
Wrong approach:class ParentCoordinator { var childCoordinators = [Coordinator]() func startChild() { let child = ChildCoordinator() childCoordinators.append(child) child.start() } // No removal of child }
Correct approach:class ParentCoordinator { var childCoordinators = [Coordinator]() func startChild() { let child = ChildCoordinator() childCoordinators.append(child) child.onFinish = { [weak self, weak child] in if let child = child { self?.childCoordinators.removeAll { $0 === child } } } child.start() } }
Root cause:Forgetting to clean up child coordinators causes memory leaks and unexpected behavior.
#3Strong reference cycles between coordinators and view controllers.
Wrong approach:class Coordinator { var navigationController: UINavigationController func start() { let vc = ViewController() vc.delegate = self // strong reference navigationController.pushViewController(vc, animated: true) } }
Correct approach:class Coordinator { var navigationController: UINavigationController func start() { let vc = ViewController() vc.delegate = self // weak reference in ViewController navigationController.pushViewController(vc, animated: true) } } class ViewController: UIViewController { weak var delegate: Coordinator? }
Root cause:Not using weak references in delegates causes retain cycles and memory leaks.
Key Takeaways
The Coordinator pattern separates navigation logic from view controllers, making code cleaner and easier to maintain.
Using multiple coordinators for different app flows keeps navigation modular and scalable.
Communication between coordinators should use delegation or callbacks to avoid tight coupling.
Proper memory management with weak references and removing child coordinators prevents leaks.
The pattern is flexible and can be adapted to various navigation styles and architectures.

Practice

(1/5)
1. What is the main purpose of the Coordinator pattern in iOS development?
easy
A. To separate navigation logic from view controllers
B. To manage data persistence
C. To handle network requests
D. To style UI components

Solution

  1. Step 1: Understand the role of Coordinator

    The Coordinator pattern is designed to manage navigation and screen flow in an app.
  2. Step 2: Compare with other options

    Data persistence, network requests, and UI styling are handled by other parts of the app, not the Coordinator.
  3. Final Answer:

    To separate navigation logic from view controllers -> Option A
  4. Quick Check:

    Coordinator = navigation separation [OK]
Hint: Coordinator handles navigation, not data or UI [OK]
Common Mistakes:
  • Confusing Coordinator with data managers
  • Thinking Coordinator styles UI
  • Assuming Coordinator handles network calls
2. Which of the following is the correct way to declare a basic Coordinator protocol in Swift?
easy
A. protocol Coordinator { func start() }
B. class Coordinator { func start() {} }
C. struct Coordinator { var start: () -> Void }
D. enum Coordinator { case start }

Solution

  1. Step 1: Identify protocol syntax

    In Swift, protocols are declared with the protocol keyword and define method signatures without implementation.
  2. Step 2: Check each option

    protocol Coordinator { func start() } correctly declares a protocol with a start() method signature. class Coordinator { func start() {} } is a class, not a protocol. struct Coordinator { var start: () -> Void } is a struct with a closure property, not a protocol. enum Coordinator { case start } is an enum, which is not used for protocols.
  3. Final Answer:

    protocol Coordinator { func start() } -> Option A
  4. Quick Check:

    Protocol syntax = protocol Coordinator { func start() } [OK]
Hint: Protocols use 'protocol' keyword and method signatures only [OK]
Common Mistakes:
  • Using class instead of protocol
  • Adding method implementation in protocol
  • Confusing struct or enum with protocol
3. Given this Coordinator code snippet, what will be printed when appCoordinator.start() is called?
protocol Coordinator {
  func start()
}

class AppCoordinator: Coordinator {
  func start() {
    print("App started")
  }
}

let appCoordinator = AppCoordinator()
appCoordinator.start()
medium
A. start() method not found
B. No output
C. Compilation error
D. App started

Solution

  1. Step 1: Analyze the class and method

    The AppCoordinator class implements the start() method which prints "App started".
  2. Step 2: Check method call

    Calling appCoordinator.start() executes the print statement inside start().
  3. Final Answer:

    App started -> Option D
  4. Quick Check:

    Method call prints message [OK]
Hint: Calling start() runs its print statement [OK]
Common Mistakes:
  • Assuming start() is missing
  • Thinking code won't compile
  • Expecting no output
4. Identify the error in this Coordinator implementation:
protocol Coordinator {
  func start()
}

class MainCoordinator: Coordinator {
  func start() {
    print("Starting main flow")
  }

  func startFlow() {
    print("Flow started")
  }
}
medium
A. startFlow() method is not part of the protocol
B. No error, code is correct
C. Missing start() method implementation
D. Protocol should be a class, not protocol

Solution

  1. Step 1: Check protocol conformance

    The MainCoordinator class implements the required start() method from the Coordinator protocol.
  2. Step 2: Verify additional methods

    Having extra methods like startFlow() is allowed and does not cause errors.
  3. Final Answer:

    No error, code is correct -> Option B
  4. Quick Check:

    Extra methods allowed, required methods implemented [OK]
Hint: Extra methods don't cause errors if required ones exist [OK]
Common Mistakes:
  • Thinking extra methods must be in protocol
  • Assuming missing methods cause errors
  • Confusing protocol with class requirements
5. You want to start a login flow from your MainCoordinator and then show the home screen after login. Which approach best uses the Coordinator pattern to handle this navigation?
hard
A. LoginCoordinator handles login and home screens without notifying MainCoordinator
B. MainCoordinator directly pushes login and home view controllers without coordinators
C. MainCoordinator creates and starts a LoginCoordinator, then listens for login completion to start HomeCoordinator
D. Use a single view controller to manage login and home screens

Solution

  1. Step 1: Understand Coordinator responsibilities

    MainCoordinator should manage the overall flow and delegate specific flows to child coordinators like LoginCoordinator.
  2. Step 2: Evaluate navigation flow

    MainCoordinator creates LoginCoordinator to handle login, then listens for completion to start HomeCoordinator, keeping navigation logic centralized.
  3. Step 3: Compare other options

    The other options break separation of concerns or do not coordinate flows properly.
  4. Final Answer:

    MainCoordinator creates and starts a LoginCoordinator, then listens for login completion to start HomeCoordinator -> Option C
  5. Quick Check:

    Parent coordinator manages child flows [OK]
Hint: Parent coordinator controls child coordinators and flow [OK]
Common Mistakes:
  • Handling navigation only in view controllers
  • Child coordinators acting independently
  • Mixing multiple screens in one view controller
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard