Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 오블완
- 코딩공부
- SWIFT
- 플러터
- MVVM-C
- 자바공부
- 영화리뷰
- 자바 스터디
- 리뷰
- IOS
- 일기
- 키노
- sopt ios
- 영화
- 영화기록
- Flutter Toy Project
- SOPT
- 새벽녘 소소한 기록
- 티스토리챌린지
- 토이프로젝트
- 백준
- sopt 35기
- java
- swift concurrency
- 프로그래머스
- 자바
- 독서일기
- 영화일기
- 영화후기
- Flutter
Archives
- Today
- Total
새벽의 기록
[iOS] TCA Zero-to-Hero #5 - Composition: 쪼개진 뷰와 리듀서 합치기 (Scope) 본문
목표: 지금껏 만든 카운터를 앱의 첫 번째 탭에 넣고, 두 번째 탭에는 새로운 기능을 넣어서 진짜 앱다운 구조를 만든다.
ProfileFeature.swift 만들기
import SwiftUI
import ComposableArchitecture
@Reducer
struct ProfileFeature {
@ObservableState
struct State: Equatable {
var nickname = "Guest"
}
enum Action: BindableAction {
case binding(BindingAction<State>)
}
var body: some Reducer<State, Action> {
BindingReducer()
}
}
struct ProfileView: View {
@Bindable var store: StoreOf<ProfileFeature>
var body: some View {
Form {
Section {
TextField("닉네임", text: $store.nickname)
Text("반갑습니다, \(store.nickname)님! 👋")
} header: {
Text("내 정보")
}
}
}
}
CounterFeature와 ProfileFeature를 관리할 상위 리듀서 AppFeature.swift 만들기
import SwiftUI
import ComposableArchitecture
@Reducer
struct AppFeature {
@ObservableState
struct State: Equatable {
var tab1 = CounterFeature.State()
var tab2 = ProfileFeature.State()
}
enum Action {
case tab1(CounterFeature.Action)
case tab2(ProfileFeature.Action)
}
var body: some Reducer<State, Action> {
Scope(state: \.tab1, action: \.tab1) {
CounterFeature()
}
Scope(state: \.tab2, action: \.tab2) {
ProfileFeature()
}
// 자식들이 뭘 하든 부모가 감시하고 싶을 때 여기에 작성.
Reduce { (state: inout State, action: Action) in
switch action {
// 자식(tab1)에게서 incrementButtonTapped 액션이 발생하면
case .tab1(.incrementButtonTapped):
// 부모가 개입해서 다른 자식(tab2)의 State를 수정
if state.tab1.count >= 10 {
state.tab2.nickname = "숫자 세기 고수 🏅"
} else {
state.tab2.nickname = "게스트"
}
return .none
default:
return .none
}
}
}
}
struct AppView: View {
let store: StoreOf<AppFeature>
var body: some View {
TabView {
CounterView(store: store.scope(state: \.tab1, action: \.tab1))
.tabItem {
Label("카운터", systemImage: "number.circle")
}
ProfileView(store: store.scope(state: \.tab2, action: \.tab2))
.tabItem {
Label("프로필", systemImage: "person.circle")
}
}
}
}
scope: 부모와 자식을 연결하는 다리
"tab1 액션이 들어오면 -> tab1 State를 가지고 -> CounterFeature 로직을 돌려라"

근데 이 부분에서

이런 식으로 타입추론을 밤티처럼 해서 클로저에 타입을 직접 명시하긴 했는데, tca의 고질병인가??
이거 생각보다 되게 불편하네. 매번 타입을 명확하게 명시해줘야 하는건가.... 흠
앱 시작점 바꾸기
import SwiftUI
import ComposableArchitecture
@main
struct TCA_Simple_TutorialApp: App {
static let store = Store(initialState: AppFeature.State()) {
AppFeature()
._printChanges()
}
var body: some Scene {
WindowGroup {
AppView(store: TCA_Simple_TutorialApp.store)
}
}
}

'[iOS]' 카테고리의 다른 글
| [iOS] 노치 시뮬레이터에서 발생하는 rdar:45025538 버그 (0) | 2026.01.06 |
|---|---|
| [iOS] TCA Zero-to-Hero #4 - Dependencies: 의존성 주입과 통제 (0) | 2025.12.26 |
| [iOS] TCA Zero-to-Hero #3 - Binding: TextField, Toggle 등 UI 바인딩 처리 (0) | 2025.12.21 |
| [iOS] TCA Zero-to-Hero #2 - Effect: 비동기 처리와 외부 통신 (0) | 2025.12.21 |
| [iOS] TCA Zero-to-Hero #1 - State, Action, Reducer의 단방향 데이터 흐름 이해 (0) | 2025.12.20 |
Comments