- Apr 10, 2025
The Secret Sauce to Scalable SwiftUI Apps: Coordinator Pattern Explained
- DevTechie
- SwiftUI
Audio overview
The Coordinator pattern has been a popular in UIKit-based iOS applications for years, helping developers manage navigation flow and decouple view controllers. As SwiftUI continues to mature, developers have been adapting this pattern to fit SwiftUI’s declarative paradigm and state-driven approach. This article explores how to implement the Coordinator pattern in SwiftUI applications, from basic concepts to complex implementations, all within the context of MVVM architecture.
The Coordinator pattern centralizes navigation logic by removing it from individual views and placing it in dedicated coordinator objects. This separation , increases the code reusability, improves testability, creates cleaner, more focused views and makes navigation flows easier to modify and maintain.
Let’s begin with an example to illustrate this concept. We will start by defining an enum and list all the pages or views that the application will have. For simplicity, we will have only two pages.
enum Page: Hashable {
case home
case detail(course: Course)
}Next, we will create an AppCoordinator that will centralize the navigation logic and publish NavigationPath.
import Observation
@Observable
final class AppCoordinator {
var path = NavigationPath()We will add functions to show course details, go back and go to the root view.
The goToRoot function has been added to demonstrate the concept of jumping directly from deeply nested paths to the root view. However, this function will not be used in this application.
@Observable
final class AppCoordinator {
var path = NavigationPath()
func showDetail(course: Course) {
path.append(Page.detail(course: course))
}
func goBack() {
path.removeLast()
}
func goToRoot() {
path.removeLast(path.count)
}Thanks to the ViewBuilder attribute, we can easily navigate between pages in our SwiftUI application by returning SwiftUI views from the coordinator.
@Observable
final class AppCoordinator {
var path = NavigationPath()
func showDetail(course: Course) {
path.append(Page.detail(course: course))
}
func goBack() {
path.removeLast()
}
func goToRoot() {
path.removeLast(path.count)
}
@ViewBuilder
func build(page: Page) -> some View {
switch page {
case .home:
HomeView()
.environment(\.coordinator, self)
case .detail(let course):
DetailView(course: course)
.environment(\.coordinator, self)
}
}
}Let’s add a new Environment key so we can access coordinator directly from the EnvironmentValues using keypath.
struct CoordinatorKey: EnvironmentKey {
static let defaultValue: AppCoordinator = AppCoordinator()
}
extension EnvironmentValues {
var coordinator: AppCoordinator {
get { self[CoordinatorKey.self] }
set { self[CoordinatorKey.self] = newValue }
}
}Next, we will design the primary landing page/view. This view will serve as the navigation hub and will host all the pages within the app.
struct CoordinatorMainView: View {
@State var coordinator = AppCoordinator()
var body: some View {
NavigationStack(path: $coordinator.path) {
coordinator.build(page: .home)
.navigationDestination(for: Page.self) { page in
coordinator.build(page: page)
}
}
}
}Last but not the least, let’s build Home and Detail views.
struct HomeView: View {
@Environment(\.coordinator) var coordinator: AppCoordinator
let courses = Course.courses
var body: some View {
List(courses) { course in
Button {
coordinator.showDetail(course: course)
} label: {
CourseCardView(course: course)
}
}
.listStyle(.plain)
.navigationTitle("DevTechie Courses")
}
}
struct DetailView: View {
let course: Course
@Environment(\.coordinator) var coordinator: AppCoordinator
var body: some View {
VStack(spacing: 20) {
Text(course.name)
.font(.title2)
.multilineTextAlignment(.center)
Text("💰 Price: \(course.price)")
Text("📚 Lessons: \(course.lessons)")
Button("Go Back") {
coordinator.goBack()
}
.buttonStyle(.bordered)
}
.padding()
.navigationTitle("Course Details")
}
}Here is the course data structure used(can be found here as well)
struct Course: Identifiable, Hashable{
let id = UUID()
var name: String
var price: String
var lessons: Int
}
extension Course {
static let courses: [Course] = [
Course(name: "Background Timer App in SwiftUI", price: "$9.99", lessons: 11),
Course(name: "iOS 18 HealthKit: Create Interactive Dashboards with SwiftUI", price: "$9.99", lessons: 15),
Course(name: "Build a Powerful Document Scanner with SwiftUI in iOS 18 : A Complete Guide", price: "$9.99", lessons: 14),
Course(name: "Weather App using CoreLocation, URLSession, UIKit, iOS 18", price: "$9.99", lessons: 11),
Course(name: "Mini Apps in iOS", price: "$9.99", lessons: 3),
Course(name: "Build Exchange Rate App: SwiftUI, iOS 18, Combine, MVVM", price: "$9.99", lessons: 10),
Course(name: "Practical SwiftData in SwiftUI & iOS 17", price: "$9.99", lessons: 38),
Course(name: "Building Complete Goals App in SwiftUI 5 & iOS 17", price: "$9.99", lessons: 35),
Course(name: "Mastering PhaseAnimation in SwiftUI 5 & iOS 17", price: "$9.99", lessons: 6),
Course(name: "ScrollViews in iOS 17 & SwiftUI 5", price: "$9.99", lessons: 14),
Course(name: "Mastering CoreImage in UIKit", price: "$9.99", lessons: 13),
Course(name: "Build Complete TaskList App in UIKit, CoreData, MVVM, Lottie", price: "$9.99", lessons: 22),
Course(name: "Favorite Places App in iOS, SwiftUI, MVVM, CoreData", price: "$9.99", lessons: 19),
Course(name: "Build WatchOS Timer App in SwiftUI 4 using MVVM", price: "$9.99", lessons: 10),
Course(name: "SwiftUI Graphics Programming With Example", price: "$9.99", lessons: 7),
Course(name: "Birthday App using Core Data with CRUD : iOS 16 & Swift UI 4", price: "$9.99", lessons: 16),
Course(name: "Mastering WidgetKit in SwiftUI 4, iOS 16", price: "$9.99", lessons: 145),
Course(name: "Mastering Charts Framework in SwiftUI 4 & iOS 16", price: "$9.99", lessons: 34),
Course(name: "Photo Gallery App in SwiftUI 4, iOS 16 & PhotosPicker", price: "$9.99", lessons: 10),
Course(name: "What's in in SwiftUI 4: Learn by Building 15 Examples", price: "$9.99", lessons: 15),
Course(name: "Introducing Maps in SwiftUI using MapKit", price: "$9.99", lessons: 9),
Course(name: "Complete E-Commerce App in SwiftUI 3, iOS 15 and Apple Pay", price: "$9.99", lessons: 17),
Course(name: "Food Recipes App in SwiftUI 3 & iOS 15 with Lottie Animation", price: "$9.99", lessons: 17),
Course(name: "Build Strava Clone in iOS using MapKit, Realm and UIKit", price: "$9.99", lessons: 16),
Course(name: "Top Destinations App in SwiftUI w/ Maps & Lottie Animation", price: "$9.99", lessons: 12),
Course(name: "Complete Reference for Text View in SwiftUI 3 and iOS 15", price: "$9.99", lessons: 28),
Course(name: "HealthKit Integration in SwiftUI", price: "$9.99", lessons: 10),
Course(name: "DevTechie Video Courses App in SwiftUI, AVPlayer, MVVM, iOS", price: "$9.99", lessons: 12),
Course(name: "Pantry Management App using MVVM, SwiftUI 2, iOS 14, Firebase Firestore and Analytics", price: "$9.99", lessons: 18),
Course(name: "Pantry Management App using MVVM, SwiftUI 3, iOS 15 & Firebase", price: "$9.99", lessons: 17),
Course(name: "Mastering SwiftUI 3: iOS App Development Bootcamp", price: "$9.99", lessons: 61),
Course(name: "iOS 15 Widgets in SwiftUI 3 & WidgetKit", price: "$9.99", lessons: 10),
Course(name: "Disney Plus Clone in SwiftUI with Remote Url Video Player", price: "$9.99", lessons: 27),
Course(name: "Goals App: SwiftUI 3, iOS 15, Protocols, MVVM, Firebase", price: "$9.99", lessons: 21)
]
}Supplement views borrowed from Mastering Searchable article.
struct CourseTitleCard: View {
var title: String
var subtitle: String
var body: some View {
ZStack {
MeshGradient(width: 2, height: 2, points: [
[0, 0], [1, 0], [0, 1], [1, 1]
], colors: [.orange, .pink, .purple, .blue])
.blur(radius: 10)
.ignoresSafeArea()
VStack(spacing: 16) {
Text(title)
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundStyle(.white)
Text(subtitle)
.font(.title3)
.foregroundStyle(.white.gradient)
.multilineTextAlignment(.center)
}
.padding()
.cornerRadius(16)
}
.frame(height: 250)
.clipShape(.rect(topLeadingRadius: 40, bottomTrailingRadius: 40))
}
}
struct CourseCardView: View {
var course: Course
var body: some View {
VStack(alignment: .leading, spacing: 8) {
CourseTitleCard(title: "DevTechie.com", subtitle: course.name)
HStack {
Text("\(course.lessons) Lessons")
.font(.subheadline)
.foregroundStyle(.secondary)
Spacer()
Text(course.price)
.font(.subheadline.bold())
.padding(6)
.background(Color.blue.opacity(0.1))
.foregroundStyle(.blue)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 2, y: 2)
}
}Build and run
With the coordinator in place, adding deep link support is just a few lines away. We will begin by adding a new URL type under project targets. In this instance, we will set the URLScheme to devtechie so that we can launch the app when a link like devtechie://course?id=XXXX is detected, either from Safari or a message.
After setting the URL Type, we will add a new function to the AppCoordinator to handle deep links correctly and redirect users to the appropriate page.
@Observable
final class AppCoordinator {
var path = NavigationPath()
func showDetail(course: Course) {
path.append(Page.detail(course: course))
}
func goBack() {
path.removeLast()
}
func goToRoot() {
path.removeLast(path.count)
}
func handleDeepLink(_ url: URL) {
guard url.scheme == "devtechie",
url.host == "course",
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let idQuery = components.queryItems?.first(where: { $0.name == "id" }) else { return }
if let course = Course.courses.first(where: { $0.id == idQuery.value }) {
path.append(Page.detail(course: course))
}
}
@ViewBuilder
func build(page: Page) -> some View {
switch page {
case .home:
HomeView()
.environment(\.coordinator, self)
case .detail(let course):
DetailView(course: course)
.environment(\.coordinator, self)
}
}
}We also need to add the onOpenURL modifier to the main view so that we can handle deep links by calling the function defined in the coordinator class.
struct CoordinatorMainView: View {
@State var coordinator = AppCoordinator()
var body: some View {
NavigationStack(path: $coordinator.path) {
coordinator.build(page: .home)
.navigationDestination(for: Page.self) { page in
coordinator.build(page: page)
}
}
.onOpenURL { url in
coordinator.handleDeepLink(url)
}
}
}We need to modify the Course data structure to replace the UUID type for the id with a String type. This will allow us to have a shorter identifier for ease of use. Additionally, we will modify the code to generate and populate unique IDs for the courses.
struct Course: Identifiable, Hashable{
let id: String
var name: String
var price: String
var lessons: Int
}
extension Course {
static let courses: [Course] = [
Course(id: "0HEt0i", name: "Background Timer App in SwiftUI", price: "$9.99", lessons: 11),
Course(id: "1FEXz5", name: "iOS 18 HealthKit: Create Interactive Dashboards with SwiftUI", price: "$9.99", lessons: 15),
Course(id: "4ptAPE", name: "Build a Powerful Document Scanner with SwiftUI in iOS 18 : A Complete Guide", price: "$9.99", lessons: 14),
Course(id: "3TIGnt", name: "Weather App using CoreLocation, URLSession, UIKit, iOS 18", price: "$9.99", lessons: 11),
Course(id: "12fc5N", name: "Mini Apps in iOS", price: "$9.99", lessons: 3),
Course(id: "1K8MQd", name: "Build Exchange Rate App: SwiftUI, iOS 18, Combine, MVVM", price: "$9.99", lessons: 10),
Course(id: "2nGlGy", name: "Practical SwiftData in SwiftUI & iOS 17", price: "$9.99", lessons: 38),
Course(id: "3bsZ9J", name: "Building Complete Goals App in SwiftUI 5 & iOS 17", price: "$9.99", lessons: 35),
Course(id: "0v8FSW", name: "Mastering PhaseAnimation in SwiftUI 5 & iOS 17", price: "$9.99", lessons: 6),
Course(id: "4TG4Fw", name: "ScrollViews in iOS 17 & SwiftUI 5", price: "$9.99", lessons: 14),
Course(id: "1JvC0K", name: "Mastering CoreImage in UIKit", price: "$9.99", lessons: 13),
Course(id: "1mvPXb", name: "Build Complete TaskList App in UIKit, CoreData, MVVM, Lottie", price: "$9.99", lessons: 22),
Course(id: "3eZWXW", name: "Favorite Places App in iOS, SwiftUI, MVVM, CoreData", price: "$9.99", lessons: 19),
Course(id: "4WHdwl", name: "Build WatchOS Timer App in SwiftUI 4 using MVVM", price: "$9.99", lessons: 10),
Course(id: "2Fh8EK", name: "SwiftUI Graphics Programming With Example", price: "$9.99", lessons: 7),
Course(id: "2kGcbX", name: "Birthday App using Core Data with CRUD : iOS 16 & Swift UI 4", price: "$9.99", lessons: 16),
Course(id: "3ov7Lk", name: "Mastering WidgetKit in SwiftUI 4, iOS 16", price: "$9.99", lessons: 145),
Course(id: "1uZrHe", name: "Mastering Charts Framework in SwiftUI 4 & iOS 16", price: "$9.99", lessons: 34),
Course(id: "3vnN68", name: "Photo Gallery App in SwiftUI 4, iOS 16 & PhotosPicker", price: "$9.99", lessons: 10),
Course(id: "4M8Y8q", name: "What's in in SwiftUI 4: Learn by Building 15 Examples", price: "$9.99", lessons: 15),
Course(id: "1OP7sQ", name: "Introducing Maps in SwiftUI using MapKit", price: "$9.99", lessons: 9),
Course(id: "2AhgNk", name: "Complete E-Commerce App in SwiftUI 3, iOS 15 and Apple Pay", price: "$9.99", lessons: 17),
Course(id: "2NSXfd", name: "Food Recipes App in SwiftUI 3 & iOS 15 with Lottie Animation", price: "$9.99", lessons: 17),
Course(id: "1BKrKh", name: "Build Strava Clone in iOS using MapKit, Realm and UIKit", price: "$9.99", lessons: 16),
Course(id: "0C8nJS", name: "Top Destinations App in SwiftUI w/ Maps & Lottie Animation", price: "$9.99", lessons: 12),
Course(id: "4R2tTx", name: "Complete Reference for Text View in SwiftUI 3 and iOS 15", price: "$9.99", lessons: 28),
Course(id: "0ghlj9", name: "HealthKit Integration in SwiftUI", price: "$9.99", lessons: 10),
Course(id: "3vxJvt", name: "DevTechie Video Courses App in SwiftUI, AVPlayer, MVVM, iOS", price: "$9.99", lessons: 12),
Course(id: "0i4dsx", name: "Pantry Management App using MVVM, SwiftUI 2, iOS 14, Firebase Firestore and Analytics", price: "$9.99", lessons: 18),
Course(id: "3R6EiW", name: "Pantry Management App using MVVM, SwiftUI 3, iOS 15 & Firebase", price: "$9.99", lessons: 17),
Course(id: "0SccEn", name: "Mastering SwiftUI 3: iOS App Development Bootcamp", price: "$9.99", lessons: 61),
Course(id: "1gAmB1", name: "iOS 15 Widgets in SwiftUI 3 & WidgetKit", price: "$9.99", lessons: 10),
Course(id: "3GVgIX", name: "Disney Plus Clone in SwiftUI with Remote Url Video Player", price: "$9.99", lessons: 27),
Course(id: "3cHGb4", name: "Goals App: SwiftUI 3, iOS 15, Protocols, MVVM, Firebase", price: "$9.99", lessons: 21)
]
}Our app is now ready for managing internal links as well as external deep links. So build and run
Handling of internal navigation:
Handling of external navigation
Visit us at https://www.devtechie.com



