apple/TCA

[TCA] Binding

lgvv 2023. 9. 27. 00:01

[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()가 수행되어 문제가 발생할 수 있음.

 

 

 

 

 

(참고)

 

https://github.com/pointfreeco/swift-composable-architecture/blob/main/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Basics.swift

 

https://github.com/pointfreeco/swift-composable-architecture/blob/main/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift

 

https://pointfreeco.github.io/swift-composable-architecture/0.54.0/documentation/composablearchitecture/bindingreducer

 

Documentation

 

pointfreeco.github.io

 

 

 

'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