
안녕하세요. 별똥별🌠입니다.
지난 시간에는 TCA Case Studies의 또 다른 사례를 통해 Composable Architecture를 학습했었죠. 이번 시간에는 양방향 바인딩(Bidirectional Binding)을 활용하여 단방향 데이터 흐름을 유지하면서도 SwiftUI의 강력한 바인딩 기능을 사용하는 방법을 소개하겠습니다.
SwiftUI는 TextField, Toggle, Slider 등 여러 UI 요소에서 양방향 바인딩을 제공합니다. 하지만, TCA는 모든 상태 변경이 Reducer를 통해 이루어지는 단방향 데이터 흐름을 기반으로 합니다. 이 둘을 결합하려면 BindableState와 sending 메서드를 활용해 상태를 안전하게 관리해야 합니다.
BindingBasics Reducer는 UI 요소(TextField, Toggle, Stepper, Slider) 각각에 필요한 상태와 액션을 정의하고, 상태 변경 로직을 처리합니다.
@Reducer
struct BindingBasics {
@ObservableState
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)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
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: isOn):
state.toggleIsOn = isOn
return .none
}
}
}
}
💡 주요 포인트
- State: UI의 각 요소(TextField, Slider 등)에 필요한 값을 정의합니다.
- Action: 사용자의 입력 이벤트를 처리하기 위한 액션을 정의합니다.
- Reducer: 상태 변경 로직을 처리하며, 예를 들어 stepCount가 변경되면 sliderValue가 stepCount를 초과하지 않도록 제약을 추가합니다.
BindingBasicsView는 @Bindable 스토어를 사용해 UI와 상태를 연결합니다. 이를 통해 SwiftUI의 양방향 바인딩을 단방향 데이터 흐름 안에 녹여낼 수 있습니다.
struct BindingBasicsView: View {
@Bindable var store: StoreOf<BindingBasics>
var body: some View {
Form {
Section {
AboutView(readMe: readMe)
}
// TextField
HStack {
TextField("Type here", text: $store.text.sending(\.textChanged))
.disableAutocorrection(true)
.foregroundStyle(store.toggleIsOn ? .secondary : .primary)
Text(alternate(store.text))
}
.disabled(store.toggleIsOn)
// Toggle
Toggle("Disable other controls", isOn: $store.toggleIsOn.sending(\.toggleChanged))
// Stepper
Stepper("Max slider value: \(store.stepCount)", value: $store.stepCount.sending(\.stepCountChanged), in: 0...100)
.disabled(store.toggleIsOn)
// Slider
HStack {
Text("Slider value: \(Int(store.sliderValue))")
Slider(value: $store.sliderValue.sending(\.sliderValueChanged), in: 0...Double(store.stepCount))
.tint(.accentColor)
}
.disabled(store.toggleIsOn)
}
.monospacedDigit()
.navigationTitle("Bindings basics")
}
}
💡 주요 포인트
- TextField:
text 상태를 @Bindable로 연결하며, 입력값 변화는 .textChanged 액션으로 처리됩니다.- Toggle:
다른 컨트롤을 활성화/비활성화하는 로직을 toggleIsOn 상태로 관리합니다.- Stepper와 Slider:
stepCount는 Slider의 최대값으로 동작하며, Slider는 상태를 변경하면서 stepCount를 초과하지 않도록 제한합니다.
Preview를 통해 UI와 상태 변경을 빠르게 테스트합니다.
#Preview {
NavigationStack {
BindingBasicsView(store: .init(initialState: BindingBasics.State()) {
BindingBasics()
})
}
}

- TCA에서 양방향 바인딩 처리
@Bindable과 sending 메서드를 통해 SwiftUI의 양방향 바인딩과 TCA의 단방향 데이터 흐름을 통합할 수 있습니다.
모든 상태 변경은 Reducer를 통해 이루어지므로 상태 관리의 예측 가능성과 유지보수성을 높일 수 있습니다.
- 상태와 액션의 연결
상태 변경 로직은 Reducer에서 처리하며, 상태와 액션을 명확히 분리합니다.
UI 요소의 값 변화는 Action으로 전송되고, Reducer에서 이를 처리한 후 다시 View에 반영됩니다.
- 효율적인 상태 관리
toggleIsOn 상태를 통해 다른 컨트롤(TextField, Slider 등)을 활성화/비활성화하는 로직을 간결하게 구현할 수 있습니다.
이 예제는 TCA에서 SwiftUI의 강력한 바인딩 기능을 어떻게 단방향 데이터 흐름에 적합하게 구현할 수 있는지를 잘 보여줍니다. 이번 포스팅을 통해 TCA의 활용 방법을 더욱 깊이 이해하길 바랍니다! 😊