[TCA] Binding
TCA의 Binding 방법 정리
목차
- TCA Binding Basic 예제
- TCA BindingState를 사용한 예제
- BidingReducer()를 가장 상단에 작성하는 이유
- TCA Binding Basic 예제
아래 코드는 TCA를 사용할 때 가장 기본적인 방법.
// MARK: - Feature domain
struct BindingBasics: Reducer {
struct State: Equatable {
var sliderValue = 5.0
var stepCount = 10
var text = ""
var toggleIsOn = false
}
enum Action {
case sliderValueChanged(Double)
case stepCountChanged(Int)
case textChanged(String)
case toggleChanged(isOn: Bool)
}
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case let .sliderValueChanged(value):
state.sliderValue = value
return .none
case let .stepCountChanged(count):
state.sliderValue = .minimum(state.sliderValue, Double(count))
state.stepCount = count
return .none
case let .textChanged(text):
state.text = text
return .none
case let .toggleChanged(isOn):
state.toggleIsOn = isOn
return .none
}
}
}
// MARK: - Feature view
struct BindingBasicsView: View {
let store: StoreOf<BindingBasics>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
Form {
Section {
AboutView(readMe: readMe)
}
HStack {
TextField(
"Type here",
text: viewStore.binding(get: \.text, send: BindingBasics.Action.textChanged)
)
.disableAutocorrection(true)
.foregroundStyle(viewStore.toggleIsOn ? Color.secondary : .primary)
Text(alternate(viewStore.text))
}
.disabled(viewStore.toggleIsOn)
Toggle(
"Disable other controls",
isOn: viewStore.binding(get: \.toggleIsOn, send: BindingBasics.Action.toggleChanged)
.resignFirstResponder()
)
Stepper(
"Max slider value: \(viewStore.stepCount)",
value: viewStore.binding(get: \.stepCount, send: BindingBasics.Action.stepCountChanged),
in: 0...100
)
.disabled(viewStore.toggleIsOn)
HStack {
Text("Slider value: \(Int(viewStore.sliderValue))")
Slider(
value: viewStore.binding(
get: \.sliderValue,
send: BindingBasics.Action.sliderValueChanged
),
in: 0...Double(viewStore.stepCount)
)
.tint(.accentColor)
}
.disabled(viewStore.toggleIsOn)
}
}
.monospacedDigit()
.navigationTitle("Bindings basics")
}
}
private func alternate(_ string: String) -> String {
string
.enumerated()
.map { idx, char in
idx.isMultiple(of: 2)
? char.uppercased()
: char.lowercased()
}
.joined()
}
위의 코드는 View에서 Binding을 하는 부분의 코드가 지저분 함. 이를 개선하고자 수정.
- TCA BindingState를 사용한 예제
Basic 예제에서 부분.
- 1. Reducer의 State에 Binding을 해야하는 프로퍼티를 @BindingState로 선언
- 2.Reducer의 Action에 채택이후 `case binding(BindingAction<State>)`를 선언
- 3. `var body: some View { ... }` 부분에 BidingReducer()를 추가.
- 주의: `Reduce { state, action in ... }`보다 상위에 추가되어야 함.
- 또한 func Reducer( ... ) { } 가 아닌 프로퍼티 body: Reduce 에서만 사용해야 함.
- 자세한 사항은 코드의 주석 참고
// MARK: - Feature domain
struct BindingForm: Reducer {
struct State: Equatable {
// ✅ 1. @BindingState로 추가
@BindingState var sliderValue = 5.0
@BindingState var stepCount = 10
@BindingState var text = ""
@BindingState var toggleIsOn = false
}
// ✅ 2. @BindableAction로 채택
enum Action: BindableAction, Equatable {
case binding(BindingAction<State>) // ✅ 3. case binding(BindingAction<State>)를 추가하여 bidingState를 처리
case resetButtonTapped
}
var body: some Reducer<State, Action> {
BindingReducer() // ✅ 4. BindingReducer()를 가장 상단에 추가
Reduce { state, action in
switch action {
case .binding(\.$stepCount):
// ✅ 6. binding에서 처리하고자하는 값을 매개변수로 지정하여 관리
state.sliderValue = .minimum(state.sliderValue, Double(state.stepCount))
return .none
case .binding: // ✅ 5. 반드시! case .binding: return .none
return .none
case .resetButtonTapped:
state = State()
return .none
}
}
}
}
// MARK: - Feature view
struct BindingFormView: View {
let store: StoreOf<BindingForm>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
Form {
Section {
AboutView(readMe: readMe)
}
HStack {
TextField("Type here", text: viewStore.$text)
.disableAutocorrection(true)
.foregroundStyle(viewStore.toggleIsOn ? Color.secondary : .primary)
Text(alternate(viewStore.text))
}
.disabled(viewStore.toggleIsOn)
Toggle("Disable other controls", isOn: viewStore.$toggleIsOn.resignFirstResponder())
Stepper(
"Max slider value: \(viewStore.stepCount)",
value: viewStore.$stepCount,
in: 0...100
)
.disabled(viewStore.toggleIsOn)
HStack {
Text("Slider value: \(Int(viewStore.sliderValue))")
Slider(value: viewStore.$sliderValue, in: 0...Double(viewStore.stepCount))
.tint(.accentColor)
}
.disabled(viewStore.toggleIsOn)
Button("Reset") {
viewStore.send(.resetButtonTapped)
}
.tint(.red)
}
}
.monospacedDigit()
.navigationTitle("Bindings form")
}
}
private func alternate(_ string: String) -> String {
string
.enumerated()
.map { idx, char in
idx.isMultiple(of: 2)
? char.uppercased()
: char.lowercased()
}
.joined()
}
새롭게 변경하면 View에서 binding하는 부분이 더욱 깔끔해짐.
- BidingReducer()를 가장 상단에 작성하는 이유
BidingReducer()가 Reduer { state, action in ... } 보다 아래에 있을 경우에 feature 로직이 먼저 수행되고 BidingReducer()가 수행되어 문제가 발생할 수 있음.
(참고)
'apple > TCA' 카테고리의 다른 글
[TCA] SharedState (1) | 2023.09.27 |
---|---|
[TCA] OptionalState (IfLetCase) (0) | 2023.09.27 |
[TCA] FocusState (0) | 2023.09.27 |
[TCA] Tutorial #5 (Multiple presentation destinations) (0) | 2023.09.24 |
[TCA] 공부기록 #1 (ReducerProtocol) (0) | 2023.01.16 |