화면 전환 로직을 Coordinator가 전담합니다. ViewModel은 비즈니스 로직에만 집중하고, 딥링크나 복잡한 네비게이션도 Coordinator 하나에서 관리합니다.
Coordinator — 화면 전환 로직을 하나의 객체에 집중. ViewModel이 네비게이션을 몰라도 됩니다.
Builder — buildView(for:)가 route에 맞는 View를 조립합니다.
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")
}
}NavigationLink나 sheet를 직접 몰라도 됩니다.NavigationPath는 타입 이레이저(type-erased) 스택입니다. Coordinator가 이 스택을 소유하고 push/pop을 담당합니다.buildView(for:)가 route를 받아 알맞은 View를 조립합니다. 새 화면 추가 시 Coordinator의 switch case 하나만 추가하면 됩니다.