-
SwiftUI TCA: 내비게이션 페이지 이동 (패스, 스택)공부/Swift(프로그래밍 언어) 2026. 1. 31. 00:52반응형
PointTreeCo 의 공식 예제를 바탕으로 설명합니다.
SwiftUI Case Studies: Navigation Stack
swift-composable-architecture/Examples/CaseStudies/SwiftUICaseStudies/04-NavigationStack.swift at main · pointfreeco/swift-comp
A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. - pointfreeco/swift-composable-architecture
github.com
구조
- 맨 처음에 루트 뷰 (Navigation Demo) 가 있고, 그 아래로 스크린 A, B, C 가 있습니다.
- 페이지는 A, B, C 순차적으로 이동할 수도 있고, A에서 C로 가거나, B 에서 A로 가는 등 스크린 A~C 내에서 다양한 이동 경로를 가질 수 있습니다.
- 각 뷰에는 해당 뷰에서 사용하는 도메인(피처) 구조체가 있습니다. 각각은 상태 및 액션, 리듀서를 가집니다.

NavigationDemo
도메인
@Reducer struct NavigationDemo { @Reducer enum Path { case screenA(ScreenA) case screenB(ScreenB) case screenC(ScreenC) } @ObservableState struct State: Equatable { var path = StackState<Path.State>() } enum Action { case goBackToScreen(id: StackElementID) case goToABCButtonTapped case path(StackActionOf<Path>) case popToRoot } var body: some Reducer<State, Action> { Reduce { state, action in switch action { // ... // } } .forEach(\.path, action: \.path) } } // Path.State가 Equatable 준수하도록 함 extension NavigationDemo.Path.State: Equatable {}@Reducer enum Path { ... }
- NavigationDemo 리듀서 안에 미니 리듀서 Path를 넣습니다. enum에 @Reducer를 붙이면 자동으로 상태, 액션, 리듀서가 생성됩니다.
- 각 케이스의 파라미터 값으로 하위 페이지에 대한 도메인을 지정합니다. (ScreenA는 도메인 구조체 struct ScreenA를 뜻함)
var path = StackState<Path.State>()
- 내비게이션의 경로(Path) 상태를 저장합니다.
- 경로는 순차적일수도 있고, 다양하게 가질 수 있습니다. (예: A - A - C - B - C - A 등)
enum Action { }
- goBackToScreen(id: StackElementID)
- 뒤로가기 버튼에 대한 액션 (StackElementID는 TCA에서 제공되는 아이디 타입입니다.)
- 팝(맨 끝 꺼내기): 리듀서 바디에서 state.path.pop(to: id)
- goToABCButtonTapped
- 스택에 A, B, C,를 순차적으로 넣습니다. 결과적으로 스크린 C로 이동합니다.
- 삽입: 리듀서 바디에서 state.path.append()
- path(StackActionOf<Path>)
- 자식 화면에서 발생한 액션이 전달된 경우에 실행할 액션
- popToRoot
- 스택에 있는 모든 경로를 꺼내고 메인 페이지로 이동
- 모두 제거: 리듀서 바디에서 state.path.removeAll()
리듀서
var body: some Reducer<State, Action> { Reduce { state, action in switch action { case let .goBackToScreen(id): state.path.pop(to: id) return .none case .goToABCButtonTapped: state.path.append(.screenA(ScreenA.State())) state.path.append(.screenB(ScreenB.State())) state.path.append(.screenC(ScreenC.State())) return .none case let .path(action): switch action { case .element(id: _, action: .screenB(.screenAButtonTapped)): state.path.append(.screenA(ScreenA.State())) return .none case .element(id: _, action: .screenB(.screenBButtonTapped)): state.path.append(.screenB(ScreenB.State())) return .none case .element(id: _, action: .screenB(.screenCButtonTapped)): state.path.append(.screenC(ScreenC.State())) return .none default: return .none } case .popToRoot: state.path.removeAll() return .none } } .forEach(\.path, action: \.path) }case let .path(action):
- 지금 받은 액션이 NavigationStack의 경로 관련 액션임을 의미합니다.
- 즉, 자식 화면에서 발생한 액션이 전달된 경우입니다.
case .element(id: _, action: .screenB(.screenAButtonTapped))
- 스택 안의 어떤 요소(element)의 액션
- 그 액션이 screenB에서 발생한 .screenAButtonTapped 인 경우를 찾음
- “ScreenB에서 A로 이동시키는 버튼이 눌렸다” 를 의미
.forEach(\.path, action: \.path)
- “path에 들어 있는 모든 화면을 각각의 Reducer로 돌려라” 라는 의미
- 컬렉션 형태의 상태를 각 요소별 Reducer에 연결
- path에 ScreenA, ScreenB, ScreenC가 쌓여 있으면 각각에 대해 Destination Reducer가 실행됨
- path 안의 State, 해당 State를 처리할 Reducer를 이어주는 다리 역할
- 이 부분이 없으면 State는 있으나 Reducer가 없고, Action을 받아줄 곳이 없음
- scope와의 차이: scope를 미리 만든 것이 아니라 path에 쌓일 때마다 “자동으로” 리듀서를 할당
뷰 (SwiftUI)
struct NavigationDemoView: View { @Bindable var store: StoreOf<NavigationDemo> var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { Form { Section { Text(template: readMe) } Section { NavigationLink( "Go to screen A", state: NavigationDemo.Path.State.screenA(ScreenA.State()) ) NavigationLink( "Go to screen B", state: NavigationDemo.Path.State.screenB(ScreenB.State()) ) NavigationLink( "Go to screen C", state: NavigationDemo.Path.State.screenC(ScreenC.State()) ) } Section { Button("Go to A → B → C") { store.send(.goToABCButtonTapped) } } } .navigationTitle("Root") } destination: { store in switch store.case { case let .screenA(store): ScreenAView(store: store) case let .screenB(store): ScreenBView(store: store) case let .screenC(store): ScreenCView(store: store) } } .safeAreaInset(edge: .bottom) { FloatingMenuView(store: store) } .navigationTitle("Navigation Stack") } }NavigationStack
- path에 scope를 지정 (상태 path, 액션 path 만 사용하겠다는 의미)
- 두 개의 클로저가 있는데 첫번째(root)는 화면에 보여질 View, 두 번째(destination)에는 액션이 실행되었을 경우 어느 뷰로 이동할지 지정합니다.
- store는 위에서 지정한 scope가 들어갑니다.
NavigationLink
NavigationLink( "스크린 A로", state: MainDomain.Path.State.screenA(ScreenADomain.State()) )- 첫번째 파라미터: 표시할 텍스트
- state: 해당 목적지 뷰에서 사용할 상태 지정 (도메인의 @Reducer enum Path 참조)
- B, C의 경우에도 동일한 방식으로 작성
스크린 A
도메인
일반적인 도메인입니다. (전체 코드 참조)
뷰
struct ScreenAView: View { let store: StoreOf<ScreenA> var body: some View { // .. // Section { NavigationLink( "Go to screen A", state: NavigationDemo.Path.State.screenA(ScreenA.State(count: store.count)) ) NavigationLink( "Go to screen B", state: NavigationDemo.Path.State.screenB(ScreenB.State()) ) NavigationLink( "Go to screen C", state: NavigationDemo.Path.State.screenC(ScreenC.State(count: store.count)) ) } }state: NavigationDemo.Path.State.screenA(ScreenA.State(count: store.count))
- 현재 스토어에 있는 store.count를 다른 링크로 넘길 수 있습니다.
스크린 B
도메인
일반적인 도메인입니다. (전체 코드 참조)
뷰
내비게이션을 이동하는 다른 방식
Button("Decoupled navigation to screen A") { store.send(.screenAButtonTapped) }- 시스템에 액션을 전송하고 루트 리듀서가 해당 액션을 가로채서 다음 기능을 스택에 푸시하도록 할 수 있습니다. (상단의 NavigationDemo > Reducer 참조)
var body: some Reducer<State, Action> { Reduce { state, action in switch action { // ... // case let .path(action): switch action { case .element(id: _, action: .screenB(.screenAButtonTapped)): state.path.append(.screenA(ScreenA.State())) return .none- 루트 도메인의 리듀서에 있는 case .path(let action)으로 간 뒤, .element(... action: 스크린 B에 있는 screenAButtonTapped액션)를 찾아 실행합니다.
스크린 C
전체 코드 참조 (동작 원리는 스크린 A, B 와 비슷함)
반응형'공부 > Swift(프로그래밍 언어)' 카테고리의 다른 글
Swift iOS: 이미지를 불러오는 Action Extension(액션 확장)에서 스크린샷 이미지를 인식하지 못하는 문제 해결 방법 (0) 2026.02.08 SwiftUI TCA: Alert (경고창) 만드는 방법 (0) 2026.02.05 SwiftUI: 탭바가 있는 뷰(TabView) 만들기 (구버전, 신버전) (0) 2026.02.04 SwiftUI: ‘appendInterpolation’ is deprecated 문제 해결하기 (0) 2026.01.28 Xcode Swift UIKit 프로젝트 스토리보드 없이 만드는 법 (0) 2026.01.24