Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
145: Coordinator Pattern for Navigation
I've seen this pattern in almost every junior-to-mid-level codebase I've ever reviewed: the ViewController that does everything. Specifically, I'm talking about the habit of letting your ViewControllers handle their own navigation. You'll see a line like self.navigationController?.pushViewController(DetailViewController(id: product.id), animated: true) sitting right inside a button tap handler. It seems intuitive at first, but it's a trap.
"Navigation is just a ViewController's job"
The misconception here is that since the ViewController is the one reacting to the user's tap, it should be the one deciding where the user goes next. On the surface, it works. But look at what happens as your app grows. If your ProductListViewController is responsible for pushing the ProductDetailViewController, the list screen now needs to know exactly how to initialize the detail screen, what dependencies that detail screen requires (like a database service or an API client), and how it's presented.
This creates tight coupling. If you ever want to reuse that ProductListViewController in a different part of the app—say, a "Favorites" tab—but you want it to push a different detail screen there, you're stuck. You've baked the navigation logic directly into the view logic. It makes your tests brittle and your ViewControllers bloated.
Extracting the flow into a dedicated Coordinator
The Coordinator pattern solves this by moving the "how do I get from A to B" logic into a separate object. Think of the Coordinator as the conductor of an orchestra; the musicians (the ViewControllers) play their parts, but they don't decide when the next movement starts.
To do this, we start with a protocol. I usually keep it simple: a start() method to kick off the flow. In a real-world shopping app, a ShopCoordinator might look like this:
protocol Coordinator { func start() } class ShopCoordinator: Coordinator { var navigationController: UINavigationController init(navigationController: UINavigationController) { self.navigationController = navigationController } func start() { let vc = ProductListViewController() vc.coordinator = self // We give the VC a reference back to us navigationController.pushViewController(vc, animated: false) } func showProductDetail(productId: String) { let vc = ProductDetailViewController(productId: productId) vc.coordinator = self navigationController.pushViewController(vc, animated: true) } }Notice that the
ShopCoordinatorowns theUINavigationController. It's the only object that knows about the navigation stack. The ViewControllers are now blissfully ignorant of who comes after them.Letting the ViewController ask, not command
Now, how does the
ProductListViewControlleractually trigger that navigation? Instead of pushing a new VC, it tells its coordinator that an event happened. I prefer using a simple property on the VC, though some developers prefer delegates. Here is how I'd implement the trigger in the View Controller:class ProductListViewController: UIViewController { weak var coordinator: ShopCoordinator? func didSelectProduct(_ product: Product) { // Instead of pushing, we simply notify the coordinator coordinator?.showProductDetail(productId: product.id) } }I use a
weakreference to the coordinator here. This is crucial. If the coordinator owns the navigation controller, and the navigation controller owns the view controller, and the view controller owns the coordinator... you've just created a retain cycle, and your memory will leak like a sieve.By shifting the responsibility, you've achieved something powerful:
ProductListViewControlleris now completely reusable. It doesn't care if it's being used in a Shop flow, a Search flow, or a Recommendation flow. It just says, "Hey, a product was selected," and lets the coordinator handle the routing logic based on the current context.
📋 Practical Task
Implementing a User Onboarding Flow Coordinator
You are tasked with building the navigation logic for a new user onboarding sequence. The flow must be: WelcomeViewController → AccountCreationViewController → DashboardViewController.
Requirements:
- Create an
OnboardingCoordinatorclass that conforms to aCoordinatorprotocol. - The coordinator should hold a reference to a
UINavigationController. - Implement a
start()method that pushes theWelcomeViewController. - Implement two additional methods in the coordinator:
showAccountCreation()andshowDashboard(). - In the
WelcomeViewControllerandAccountCreationViewController, add aweak var coordinatorproperty. - Inside the ViewControllers, simulate a button press (e.g., a function called
didTapNext()) that calls the appropriate method on the coordinator to move to the next screen.
Goal: Ensure that none of the ViewControllers call pushViewController directly. All navigation must be routed through the OnboardingCoordinator.
There are no comments for now.