구조 다이어그램
Coordinator ├── path (NavigationPath) ├── sheet (AppRoute?) └── buildView(for:) ← Builder manages | ViewModel ——binds——> View | "detailRequested"만 호출 실제 push/present는 Coordinator가 담당
MVVM과의 차이
  • ViewModel에서 화면 전환 코드 제거
  • Coordinator가 NavigationPath 소유
  • 딥링크 처리가 Coordinator 하나에 집중
  • Builder 패턴으로 View 조립 분리
  • AppRoute enum으로 화면을 타입 안전하게 정의
내장 디자인 패턴
Coordinator Builder

Coordinator — 화면 전환 로직을 하나의 객체에 집중. ViewModel이 네비게이션을 몰라도 됩니다.

Builder — buildView(for:)가 route에 맞는 View를 조립합니다.

 04-MVVM-Coordinator/Finish/AppCoordinator.swift
import SwiftUI

// MARK: - Coordinator (완성)

enum AppRoute: Hashable, Identifiable {
    case taskDetail(TaskItem)
    case addTask

    var id: String {
        switch self {
        case .taskDetail(let t): "detail-\(t.id)"
        case .addTask: "add"
        }
    }
}

@MainActor
final class AppCoordinator: ObservableObject {
    @Published var path = NavigationPath()
    @Published var sheet: AppRoute? = nil

    // 화면 전환 API — ViewModel은 이것만 호출
    func push(_ route: AppRoute) { path.append(route) }
    func pop() { guard !path.isEmpty else { return }; path.removeLast() }
    func present(_ route: AppRoute) { sheet = route }
    func dismissSheet() { sheet = nil }

    // Builder 패턴: route에 맞는 View를 여기서 조립
    @ViewBuilder
    func buildView(for route: AppRoute) -> some View {
        switch route {
        case .taskDetail(let task): TaskDetailView(task: task)
        case .addTask: Text("Add Task Sheet")
        }
    }
}

// ---- Root View ----

struct CoordinatorView: View {
    @StateObject private var coordinator = AppCoordinator()
    @StateObject private var viewModel = TaskListViewModel()

    var body: some View {
        NavigationStack(path: $coordinator.path) {
            CoordTaskListView(viewModel: viewModel, coordinator: coordinator)
                .navigationDestination(for: AppRoute.self) { coordinator.buildView(for: $0) }
        }
        .sheet(item: $coordinator.sheet) { coordinator.buildView(for: $0) }
        .environmentObject(coordinator)
    }
}

// ---- Task List View ----

struct CoordTaskListView: View {
    @ObservedObject var viewModel: TaskListViewModel
    @ObservedObject var coordinator: AppCoordinator

    var body: some View {
        List(viewModel.filteredTasks) { task in
            // ViewModel은 "detailRequested"만 알고,
            // 실제 push는 Coordinator가 처리
            Button { coordinator.push(.taskDetail(task)) } label: {
                Text("\(task.priority.emoji) \(task.title)")
            }
        }
        .navigationTitle("Tasks (MVVM+C)")
        .toolbar {
            Button { coordinator.present(.addTask) } label: {
                Image(systemName: "plus")
            }
        }
    }
}

// ---- Detail View ----

struct TaskDetailView: View {
    let task: TaskItem
    @EnvironmentObject var coordinator: AppCoordinator

    var body: some View {
        VStack(spacing: 16) {
            Text(task.title).font(.title)
            Text(task.priority.emoji + " " + task.priority.label)
            Text(task.isCompleted ? "Completed" : "Pending")
                .foregroundStyle(task.isCompleted ? .green : .orange)
            Button("Go Back") { coordinator.pop() }
        }.navigationTitle("Detail")
    }
}
핵심 개념
Coordinator 패턴
화면 전환의 "누가, 어디로, 어떻게" 를 Coordinator 하나에 모읍니다. ViewModel이 NavigationLink나 sheet를 직접 몰라도 됩니다.
NavigationPath
SwiftUI의 NavigationPath는 타입 이레이저(type-erased) 스택입니다. Coordinator가 이 스택을 소유하고 push/pop을 담당합니다.
AppRoute enum
가능한 모든 화면을 enum case로 정의합니다. 컴파일 타임에 존재하지 않는 화면으로 이동하는 실수를 방지하고, 딥링크 매핑도 한 곳에서 처리합니다.
Builder 패턴
buildView(for:)가 route를 받아 알맞은 View를 조립합니다. 새 화면 추가 시 Coordinator의 switch case 하나만 추가하면 됩니다.