🍎

[SwiftUI]TCAを理解する:binding

2022/05/12に公開約7,000字

概要

この記事ではTCA初心者の筆者が理解を深めていくために、
pointfreeco公式のサンプルアプリを基に理解しやすく整理していきます。
今回はbindingの使用例を整理して理解していきます。

前回のpullback・combine・scopeについてはこちら

今回扱うファイル

今回は公式サンプルの以下の2つのファイルです。

Bindings-Basics

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

Bindings-Forms

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

Bindings-Basics

ここではbindingを使用した双方向のバインディングについての基礎を学びます。
実装ではTextFieldやToggle、StepperやSlider、
これらを動かした時にActionを送るだけでなく、
動かした状態もActionに引数として持たせてStateを更新しています。

State,Action

これまでと異なるのがActionに引数を持たせています。

struct BindingBasicsState: Equatable {
  var sliderValue = 5.0
  var stepCount = 10
  var text = ""
  var toggleIsOn = false
}

enum BindingBasicsAction {
  case sliderValueChanged(Double) //Actionに引数を持たせる 
  case stepCountChanged(Int)
  case textChanged(String)
  case toggleChanged(isOn: Bool)
}

Reducer

Actionの引数の値でStateを更新します。

let bindingBasicsReducer = Reducer<BindingBasicsState, BindingBasicsAction, BindingBasicsEnvironment> {
  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 // Actionの引数の値でStateを更新
    return .none

  case let .toggleChanged(isOn):
    state.toggleIsOn = isOn
    return .none
  }
}

View,Store

ここのポイントは、bindingを使用している、
viewStore.binding(get: \.text, send: BindingBasicsAction.textChanged)です。
TextFieldで入力された値をgetに、
sendでこれまでと同じようにActionに送っています。
他のTextField以外のコンポーネントも同様です。

struct BindingBasicsView: View {
  let store: Store<BindingBasicsState, BindingBasicsAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      Form {
        Section(header: Text(template: readMe, .caption)) {
          HStack {
            TextField(
              "Type here",
	      // ここのget,setのパラメータで双方向のバインディング
              text: viewStore.binding(get: \.text, send: BindingBasicsAction.textChanged)
            )
            .disableAutocorrection(true)
            .foregroundColor(viewStore.toggleIsOn ? .gray : .primary)
            Text(alternate(viewStore.text))
          }
          .disabled(viewStore.toggleIsOn)

          Toggle(
            isOn: viewStore.binding(get: \.toggleIsOn, send: BindingBasicsAction.toggleChanged)
          ) {
            Text("Disable other controls")
          }

          Stepper(
            value: viewStore.binding(
              get: \.stepCount, send: BindingBasicsAction.stepCountChanged),
            in: 0...100
          ) {
            Text("Max slider value: \(viewStore.stepCount)")
              .font(.body.monospacedDigit())
          }
          .disabled(viewStore.toggleIsOn)

          HStack {
            Text("Slider value: \(Int(viewStore.sliderValue))")
              .font(.body.monospacedDigit())
            Slider(
              value: viewStore.binding(
                get: \.sliderValue,
                send: BindingBasicsAction.sliderValueChanged
              ),
              in: 0...Double(viewStore.stepCount)
            )
          }
          .disabled(viewStore.toggleIsOn)
        }
      }
    }
    .navigationBarTitle("Bindings basics")
  }
}

Bindings-Forms

ここではBindableStateというStateの定義をすることで、
BindingActionというActionを使用し、
bindingの処理をより簡潔にすることを学びます。
前回のBindings-Basicsとの比較で分かりやすく整理していきます。

State,Action

Bindings-Basicsと異なるのは、
BindableStateのアノテーションを付与している点と、
同じ機能なのにActionのケースが少なくなっている点です。
BindableActionを継承し、
BindingActionにBindingFormStateを持つことで一括りで保持出来ます。

Bindings-Forms
  struct BindingFormState: Equatable {
    @BindableState var sliderValue = 5.0
    @BindableState var stepCount = 10
    @BindableState var text = ""
    @BindableState var toggleIsOn = false
  }

  enum BindingFormAction: BindableAction, Equatable {
    case binding(BindingAction<BindingFormState>)
    case resetButtonTapped
  }
Bindings-Basics
struct BindingBasicsState: Equatable {
  var sliderValue = 5.0
  var stepCount = 10
  var text = ""
  var toggleIsOn = false
}

enum BindingBasicsAction {
  case sliderValueChanged(Double)
  case stepCountChanged(Int)
  case textChanged(String)
  case toggleChanged(isOn: Bool)
}

Reducer

ReducerでBindings-Basicsと異なるのは、
まず各機能のアクションケースごとにStateを更新する必要が無い点と、
最後に.binding()というメソッドを使用していることです。
Reducerのロジックを実行する前に、
Stateに対してBindingActionの変更を適用する為のReducerを返します。
簡単に整理するとBindingActionを使用する場合は必須なメソッドのようです。

let bindingFormReducer = Reducer<
    BindingFormState, BindingFormAction, BindingFormEnvironment> {
    state, action, _ in
    switch action {
    case .binding(\.$stepCount):
      state.sliderValue = .minimum(state.sliderValue, Double(state.stepCount))
      return .none

    case .binding:
      return .none

    case .resetButtonTapped:
      state = .init()
      return .none
    }
  }
  .binding() // Stateに対してBindingActionの変更を適用するReducerを返す

View,Store

ここでBindings-Basicsと異なるのは、
各機能の実行アクションのところです。
例えばTextFieldの部分を見てみます。
Bindings-Basicsではgetsendでbindingしてましたが、
今回の場合はこれだけで済みます。
viewStore.binding(\.$text)

Bindings-Forms
struct BindingFormView: View {
    let store: Store<BindingFormState, BindingFormAction>

    var body: some View {
      WithViewStore(self.store) { viewStore in
        Form {
          Section(header: Text(template: readMe, .caption)) {
            HStack {
              TextField("Type here", text: viewStore.binding(\.$text))
                .disableAutocorrection(true)
                .foregroundColor(viewStore.toggleIsOn ? .gray : .primary)

              Text(alternate(viewStore.text))
            }
            .disabled(viewStore.toggleIsOn)
	    
	    // ...
Bindings-Basics
struct BindingBasicsView: View {
  let store: Store<BindingBasicsState, BindingBasicsAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      Form {
        Section(header: Text(template: readMe, .caption)) {
          HStack {
            TextField(
              "Type here",
              text: viewStore.binding(get: \.text, send: BindingBasicsAction.textChanged)
            )
            .disableAutocorrection(true)
            .foregroundColor(viewStore.toggleIsOn ? .gray : .primary)
            Text(alternate(viewStore.text))
          }
          .disabled(viewStore.toggleIsOn)

          // ...

次回

次回は様々なStateの使い方について理解していきます。
記事はこちら

Discussion

ログインするとコメントできます