🙄

TCAにおけるBindingActionとViewActionを利用したActionの整理

に公開

なぜBindingActionとViewActionが必要なのか

TCAのReducerでActionを追加していくと、Actionがかなり多くなってしまい処理がわかりにくくなってしまうことがあると思います。
TCAではこれらのActionを整理するBindingActionとViewActionなどの方法があるので、紹介していきたいと思います

1. BindingActionとは何か

BindingActionはSwiftUIの双方向バインディング(@Binding)をTCAの単方向データフローに統合するためのメカニズムです。これにより、TextField、Toggle、Sliderなどの双方向バインディングを必要とするSwiftUIコンポーネントをTCAと簡単に連携できます。

実際のユースケース

サンプルコードでは、新しいタスクのタイトル入力(newTodoTitle)とフィルタートグル(filterCompleted)にBindingActionを使用しています:

// State内での宣言
var newTodoTitle: String = ""
var filterCompleted: Bool = false

// Actionの定義部分
enum Action: ViewAction, BindableAction, Sendable {
    case binding(BindingAction<State>)
    // 他のアクション...
}

// Reducerへの組み込み
var body: some ReducerOf<Self> {
    BindingReducer()
    
    Reduce { state, action in
        switch action {
        case .binding:
            return .none
            // 他のケース...
        }
    }
}

ビューでの使用方法

ビュー側では、$storeバインディングを使って直接状態に接続します:

TextField("新しいタスク", text: $store.newTodoTitle)
    .textFieldStyle(RoundedBorderTextFieldStyle())

Toggle("完了済みのみ表示", isOn: $store.filterCompleted)
    .tint(.green)

BindingActionの実装

  1. ActionにBindableActionプロトコルを適用:
enum Action: BindableAction {
    case binding(BindingAction<State>)
    // 他のケース...
}
  1. ReducerにBindingReducerを追加:
var body: some ReducerOf<Self> {
    BindingReducer()
    Reduce { state, action in
        // 他のアクション処理...
    }
}
  1. storeを@Bindableとして宣言:
@Bindable var store: StoreOf<TodoFeature>
  1. Viewで双方向バインディングを使用:
TextField("新しいタスク", text: $store.newTodoTitle)

2. ViewActionとは何か

ViewActionは、Viewから発生するアクションを整理し、Actionの列挙型を簡潔に保つためのパターンです。これにより、Actionの一覧の中でViewからのActionを明示的に表現することができ、Actionの一覧がわかりやすくなります

実際のユースケース

サンプルコードでは、Viewから発生するすべてのActionをView列挙型内に整理しています:

enum Action: ViewAction, BindableAction, Sendable {
    case binding(BindingAction<State>)
    case view(View)
    enum View: Equatable {
        case onAppear
        case addTodoButtonTapped
        case todoCheckboxToggled(id: Todo.ID)
        case deleteTodoButtonTapped(id: Todo.ID)
    }
}

ビューでの使用方法

Viewからは@ViewActionマクロを使用して、簡潔にアクションを送信できます:

@ViewAction(for: TodoFeature.self)
struct TodoView: View {
    @Bindable var store: StoreOf<TodoFeature>
    
    var body: some View {
        Button(action: {
            send(.addTodoButtonTapped)
        }) {
            Image(systemName: "plus.circle.fill")
                .font(.title2)
                .foregroundColor(.blue)
        }
        // 他のビュー要素...
    }
}

実装手順

ViewActionの実装

  1. フィーチャーのActionにViewActionプロトコルを適用:
enum Action: ViewAction {
    case view(View)
    enum View {
    }
}
  1. ビューに@ViewActionマクロを適用:
@ViewAction(for: TodoFeature.self)
struct TodoView: View {
}
  1. send()関数を使ってアクションを送信:
Button(action: {
    send(.addTodoButtonTapped)
}) {
}

まとめ

BindingActionとViewActionによるActionの整理について紹介しました
サンプルコードも合わせて確認してください

https://github.com/entaku0818/TCA-sample

TCAのドキュメントでも記載されているのでこちらも合わせて確認してください

https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/bindingaction/

https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/viewaction

Discussion