diff --git a/TeamIntroduce/TeamIntroduce/Sources/Application/TeamIntroduceApp.swift b/TeamIntroduce/TeamIntroduce/Sources/Application/TeamIntroduceApp.swift index 3cabac3..0adc934 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Application/TeamIntroduceApp.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Application/TeamIntroduceApp.swift @@ -10,23 +10,9 @@ import SwiftData @main struct TeamIntroduceApp: App { - var sharedModelContainer: ModelContainer = { - let schema = Schema([ - Item.self, - ]) - let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) - - do { - return try ModelContainer(for: schema, configurations: [modelConfiguration]) - } catch { - fatalError("Could not create ModelContainer: \(error)") - } - }() - var body: some Scene { WindowGroup { - ContentView() + RootView() } - .modelContainer(sharedModelContainer) } } diff --git a/TeamIntroduce/TeamIntroduce/Sources/Common/Navigation/NavigationControlling.swift b/TeamIntroduce/TeamIntroduce/Sources/Common/Navigation/NavigationControlling.swift new file mode 100644 index 0000000..f430184 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Common/Navigation/NavigationControlling.swift @@ -0,0 +1,69 @@ +// +// NavigationControlling.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/11/25. +// + +import Foundation + +import Foundation +import SwiftUI + +// MARK: - Navigation 제어 프로토콜 + +/// SwiftUI `NavigationStack` 기반 화면 전환을 제어하는 공통 프로토콜입니다. +/// +/// 각 탭 또는 플로우 별로 코디네이터가 이 프로토콜을 채택하면 +/// 공통적인 `goBack`, `reset` 동작을 재사용할 수 있습니다. +protocol NavigationControlling: AnyObject { + + /// 현재 네비게이션 경로 상태입니다. + /// + /// `NavigationStack(path:)`에 바인딩되어 화면 이동을 제어합니다. + var path: NavigationPath { get set } + + /// 초기 진입 지점을 설정하는 메서드입니다. + /// + /// 각 코디네이터에서 구현되어야 하며, + /// 앱 시작 또는 탭 변경 시 루트 화면을 정의합니다. + func start() + + /// 마지막 화면을 스택에서 제거하여 이전 화면으로 이동합니다. + func goBack() + + /// 스택의 모든 경로를 제거하고 루트 화면으로 돌아갑니다. + func reset() +} + +// MARK: - NavigationControlling 기본 구현 + +extension NavigationControlling { + + /// 현재 화면을 스택에서 팝(뒤로 가기)합니다. + /// + /// - 동작: + /// - `path`가 비어있지 않은 경우, 마지막 항목을 제거하여 이전 화면으로 이동합니다. + /// + /// - 예시: + /// ```swift + /// coordinator.goBack() + /// ``` + func goBack() { + guard !path.isEmpty else { return } + path.removeLast() + } + + /// 네비게이션 스택을 초기 상태(루트)로 리셋합니다. + /// + /// - 동작: + /// - 현재 경로 배열에서 모든 화면을 제거하여, 루트 화면만 유지합니다. + /// + /// - 예시: + /// ```swift + /// coordinator.reset() + /// ``` + func reset() { + path.removeLast(path.count) + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceCoordinator.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceCoordinator.swift new file mode 100644 index 0000000..4a69d93 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceCoordinator.swift @@ -0,0 +1,75 @@ +// +// IntroduceCoordinator.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/11/25. +// + +import Combine +import SwiftUI + + +final class IntroduceCoordinator: NavigationControlling, ObservableObject { + + @Published var path = NavigationPath() + + // 액션 기반 네비게이션 + enum Action { + case start + case pop + case popToRoot + case present(_ route: IntroduceRoute) + case replaceStack(_ routes: [IntroduceRoute]) + } + + func send(_ action: Action) { + switch action { + case .start: + start() + + case .pop: + if !path.isEmpty { path.removeLast() } + + case .popToRoot: + path = .init() + + + case .present(let route): + path.append(route) + + case .replaceStack(let routes): + replaceStack(routes) + + } + } + + // MARK: - NavigationControlling 요구 구현 + func start() { + reset() + send(.present(.introduceMain)) + } + + // ⚠️ 프로토콜이 요구한다면 private 붙이면 안 됨 + func reset() { + path = .init() + } + + // 스택 교체 + func replaceStack(_ routes: [IntroduceRoute], animated: Bool = true) { + let apply = { + self.path = .init() + routes.forEach { self.path.append($0) } + } + + if animated { + withAnimation(.default) { apply() } + } else { + apply() + } + } + + // 편의 오버로드 + func replaceStack(_ route: IntroduceRoute, animated: Bool = true) { + replaceStack([route], animated: animated) + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceRoute.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceRoute.swift new file mode 100644 index 0000000..6a67d16 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceRoute.swift @@ -0,0 +1,21 @@ +// +// IntroduceRoute.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/11/25. +// + +import Foundation + +enum IntroduceRoute: Hashable { + + // 소개 페이지 메인 화면 + case introduceMain + // 팀약속 + case teamAgreement + // 팀소개 + case teamIntroduce + // 팀 블로그 + case teamBlog + +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/View/IntorduceCoordinatorView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/View/IntorduceCoordinatorView.swift new file mode 100644 index 0000000..d7d94b9 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/View/IntorduceCoordinatorView.swift @@ -0,0 +1,50 @@ +// +// IntorduceCoordinatorView.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/11/25. +// + +import SwiftUI +import SwiftData + +struct IntorduceCoordinatorView : View { + @EnvironmentObject private var coordinator: IntroduceCoordinator + var sharedModelContainer: ModelContainer = { + let schema = Schema([ + Item.self, + ]) + let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + + do { + return try ModelContainer(for: schema, configurations: [modelConfiguration]) + } catch { + fatalError("Could not create ModelContainer: \(error)") + } + }() + + var body: some View { + NavigationStack(path: $coordinator.path) { + ContentView() + .navigationDestination(for: IntroduceRoute.self, destination: makeDestination) + } + .modelContainer(sharedModelContainer) + } +} + + +extension IntorduceCoordinatorView { + @ViewBuilder + private func makeDestination(for route: IntroduceRoute) -> some View { + switch route { + case .introduceMain: + ContentView() + case .teamAgreement: + ContentView() + case .teamIntroduce: + EmptyView() + case .teamBlog: + EmptyView() + } + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/ContentView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/ContentView.swift index 1770e51..6b8e037 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/ContentView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/ContentView.swift @@ -9,53 +9,37 @@ import SwiftUI import SwiftData struct ContentView: View { - @Environment(\.modelContext) private var modelContext - @Query private var items: [Item] + @Environment(\.modelContext) private var modelContext + @Query private var items: [Item] + @EnvironmentObject private var coordinator: IntroduceCoordinator - var body: some View { - NavigationSplitView { - List { - ForEach(items) { item in - NavigationLink { - Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") - } label: { - Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) - } - } - .onDelete(perform: deleteItems) - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - EditButton() - } - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } - } - } detail: { - Text("Select an item") + + var body: some View { + VStack { + Text("main") + .onTapGesture { + coordinator.send(.present(.teamAgreement)) } } + } - private func addItem() { - withAnimation { - let newItem = Item(timestamp: Date()) - modelContext.insert(newItem) - } + private func addItem() { + withAnimation { + let newItem = Item(timestamp: Date()) + modelContext.insert(newItem) } + } - private func deleteItems(offsets: IndexSet) { - withAnimation { - for index in offsets { - modelContext.delete(items[index]) - } - } + private func deleteItems(offsets: IndexSet) { + withAnimation { + for index in offsets { + modelContext.delete(items[index]) + } } + } } #Preview { - ContentView() - .modelContainer(for: Item.self, inMemory: true) + ContentView() + .modelContainer(for: Item.self, inMemory: true) } diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/Root/RootView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/Root/RootView.swift new file mode 100644 index 0000000..f1ce0c8 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/Root/RootView.swift @@ -0,0 +1,20 @@ +// +// RootView.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/11/25. +// + +import SwiftUI + +struct RootView: View { + @StateObject var coordinator = IntroduceCoordinator() + var body: some View { + IntorduceCoordinatorView() + .environmentObject(coordinator) + } +} + +#Preview { + RootView() +}