NavigationControlling
+ 액션 기반 IntroduceCoordinator
SwiftUI의 NavigationStack
만으로 화면 전환을 하다 보면 뒤로 가기, 루트 리셋, 딥링크 같은 공통 로직이 뷰에 흩어지기 쉽습니다.
아래처럼 작은 프로토콜 + 액션 기반 코디네이터를 두면, 뷰는 의도(Action) 만 보내고 라우팅은 코디네이터가 책임집니다.
NavigationStack(path:)에 바인딩되는 path와, 공통으로 쓰는 start / goBack / reset 인터페이스를 정의합니다.
기본 구현을 extension으로 제공해, 채택 측(코디네이터)이 최소만 구현하게 합니다.
import SwiftUI
/// NavigationStack 기반 화면 전환을 제어하는 공통 프로토콜
protocol NavigationControlling: AnyObject {
var path: NavigationPath { get set } // NavigationStack(path:)
func start() // 초기 진입(루트 구성)
func goBack() // 뒤로
func reset() // 루트로
}
extension NavigationControlling {
func goBack() {
guard !path.isEmpty else { return }
path.removeLast()
}
func reset() {
path.removeLast(path.count)
}
}
라우트는 단순 enum이 유지보수에 유리합니다. 새 화면 추가 시 영향 범위가 작고, 테스트/프리뷰도 쉽습니다.
enum IntroduceRoute: Hashable {
case introduceMain
case teamAgreement
case teamIntroduce
case teamBlog
}
✍️ 단순 enum으로 유지하면 새 라우트 추가 시 영향 범위가 작고 테스트·미리보기도 편합니다.
팁: 라우트마다 연관값이 필요하면 case teamBlog(postID: String)처럼 확장하세요. Hashable만 유지되면 됩니다.
화면 전환 의도를 Action enum으로 표현합니다.
뷰는 coordinator.send(.presentDetail(.teamAgreement))처럼 의도만 보냅니다.
import SwiftUI
import Combine
final class IntroduceCoordinator: NavigationControlling, ObservableObject {
@Published var path = NavigationPath()
// 화면 전환에 대한 "의도"
enum Action {
case start
case presentMain
case presentDetail(IntroduceRoute) // 연관값으로 확장성 확보
case replace([IntroduceRoute]) // 스택 교체
case pop
case popToRoot
}
func send(_ action: Action) {
switch action {
case .start:
reset()
send(.presentMain)
case .presentMain:
path.append(.introduceMain)
case .presentDetail(let route):
path.append(route)
case .replace(let routes):
replaceStack(routes)
case .pop:
goBack()
case .popToRoot:
reset()
}
}
// MARK: - NavigationControlling
func start() {
reset()
send(.presentMain)
}
// 필요 시 외부에서 직접 호출 가능하도록 유지
func reset() { path = .init() }
}
정 흐름(루트 → 상세)을 한 번에 구성해야 할 때 씁니다.
애니메이션 on/off도 선택 가능.
extension IntroduceCoordinator {
/// 현재 스택을 전달된 라우트 목록으로 교체
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)
}
}
• 라우트가 단순 enum이라 스냅샷/내비게이션 시나리오 테스트가 수월합니다.
• 프리뷰에서 IntroduceCoordinator()를 주입해 라우팅 동작을 확인할 수 있어요.
#Preview {
IntroduceRootView()
.environmentObject(IntroduceCoordinator())
}
@MainActor
를 코디네이터에 붙이세요.case teamBlog(postID: String)
처럼 모델 키만 넘기고, 뷰에서 조회하세요.replaceStack
으로 딥링크/재진입까지 간단히 처리할 수 있습니다.