범위
이번 정리에서 볼 흐름
View 입력
-> Binding setter
-> binding Action
-> StoreOf
-> BindingReducer
-> State 변경
-> View 다시 그림
버튼 탭은 store.send로 Action을 보냈다
여기서 Binding은 SwiftUI의 Binding을 말한다
SwiftUI의 Binding 자체는 값을 읽고 쓰는 통로다
TCA에서는 Store에서 만든 Binding을 binding Action 흐름으로 연결할 수 있다
이번 예제는 기존 카운터 흐름을 유지한다
추가하는 값은 하나
step = 증가하거나 감소할 단위
이번 예제에서 볼 것은 세 가지
@Bindable var store 또는 @State var store
BindableAction
BindingReducer
iOS 16 이하에서 사용할 경우 @Bindable 대신 @Perception.Bindable을 사용한다
이때 @Bindable은 우리가 아는 SwiftUI의 @Bindable 이 맞다
iOS 17 이상
-> @Bindable
iOS 16 이하
-> @Perception.Bindable
예제 코드
카운터에 step을 추가한다
step은 Stepper로 바꿀 수 있다
@Reducer
struct CounterBindingFeature {
@ObservableState
struct State: Equatable {
var count = 0
var step = 1
}
enum Action: BindableAction {
case binding(BindingAction<State>)
case decrementButtonTapped
case incrementButtonTapped
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
break
case .decrementButtonTapped:
state.count -= state.step
case .incrementButtonTapped:
state.count += state.step
}
return .none
}
}
}
기존 카운터와 달라진 점
State에 step 추가
Action이 BindableAction 채택
binding Action 추가
BindingReducer 추가
State
@ObservableState
struct State: Equatable {
var count = 0
var step = 1
}
count는 결과 값이다
step은 사용자가 바꾸는 입력 값이다
버튼을 누르면 step만큼 값이 바뀐다
state.count += state.step
BindableAction
Store에서 만든 Binding 변경을 binding Action으로 받으려면 Action이 BindableAction을 채택한다
SwiftUI의 Binding 자체를 쓴다고 항상 필요한 것은 아니다
enum Action: BindableAction {
case binding(BindingAction<State>)
case decrementButtonTapped
case incrementButtonTapped
}
binding Action은 View의 Binding 변경을 Reducer로 전달하기 위한 Action이다
Stepper가 step을 바꾸면 이 Action 흐름을 탄다
Stepper
-> binding Action
-> BindingReducer
-> state.step 변경
BindingReducer
BindingReducer()
BindingReducer는 binding Action을 받아 State 값을 실제로 바꾼다
그래서 .binding case에서는 별도 처리를 하지 않는다
case .binding:
break
현재 기준에서 .binding은 사용자가 step을 바꿨다는 흐름만 통과시킨다
실제 step 변경은 BindingReducer가 처리한다
View에서 Binding 만들기
이 예제의 View는 전달받은 StoreOf에서 Binding을 만들기 위해 @Bindable을 사용한다
struct CounterBindingView: View {
@Bindable var store: StoreOf<CounterBindingFeature>
}
View가 Store를 직접 만들고 소유한다면 @State로도 Binding을 만들 수 있다
@State var store = Store(initialState: CounterBindingFeature.State()) {
CounterBindingFeature()
}
@Bindable은 Store를 소유하기 위한 도구라기보다 전달받은 Store에서 $store.step 같은 Binding을 만들기 위한 도구다
Stepper(value: $store.step, in: 1...10) {
Text("증가 단위 \(store.step)")
}
여기서 $store.step이 Binding이다
겉으로 보면 Stepper가 state.step을 직접 바꾸는 것처럼 보인다
하지만 실제로는 그렇지 않다
View가 만든 Binding의 setter가 Action을 보낸다
Binding 변경은 binding Action으로 들어간다
BindingReducer가 State를 바꾼다
개념적으로는 거의 이런 모양이다
Stepper(
value: Binding(
get: { store.step },
set: { store.send(.binding(.set(\.$step, $0))) }
),
in: 1...10
) {
Text("증가 단위 \(store.step)")
}
실제 예제는 이 긴 코드를 축약해서 $store.step으로 쓴 것이다
즉 send를 안 쓰는 것이 아니라 Binding 안에 send가 숨어 있다고 보면 된다
그래서 View가 State를 직접 바꾸는 구조가 아니다
View
-> Binding setter
-> store.send(.binding(...))
-> BindingReducer
-> State 변경
버튼 Action과 Binding
증가 버튼은 기존처럼 Action을 보낸다
Button {
store.send(.incrementButtonTapped)
} label: {
Image(systemName: "plus")
}
Reducer는 현재 step을 읽어서 count를 바꾼다
state.count += state.step
흐름은 이렇게 나눠진다
Stepper 조작
-> binding Action
-> state.step 변경
plus 버튼 탭
-> state.count += state.step
Binding은 버튼 Action을 대체하는 개념이 아니다
사용자가 직접 값을 바꾸는 UI를 State와 연결하는 흐름이다
정리
현재 이해한 Binding
@Bindable var store = 전달받은 Store에서 $store.step 같은 Binding을 만들기 위한 wrapper
@State var store = View가 Store를 직접 소유하면서 Binding을 만들기 위한 wrapper
BindableAction = Binding Action을 받을 수 있는 Action
BindingReducer = Binding Action으로 State를 변경하는 Reducer
카운터 예제 기준
Stepper
-> $store.step
-> store.send(.binding(...))
-> binding Action
-> BindingReducer
-> state.step 변경