[Xcode/SwiftUI] The Composable Architecture (TCA)を学んでみる (基礎の基礎)

TCAアーキテクチャとは

The Composable Architecture(TCA)は、Point-Freeが開発したSwiftUIアプリケーション開発のためのフレームワーク。単方向データフローと関数型プログラミングの原理に基づき、Testableで保守性の高いアプリケーションを構築できるとのこと。

TCAの基本概念

TCAは4つの主要なコンポーネントで構成

State(状態)

アプリケーションの現在の状態を表現する値型。UIに表示されるすべてのデータがここに含まれる。

Action(アクション)

ユーザーの操作やシステムイベントを表現する列挙型(enum)。ボタンタップ、テキスト入力、API呼び出しの結果などがアクションとして定義される。

Reducer(リデューサー)

現在のStateとActionを受け取り、新しいStateを返す純粋関数。副作用もここで管理。

Store(ストア)

StateとActionを管理し、ViewとReducerを繋ぐ役割を担う。

メモアプリの実装

  • 追加
  • 削除
  • TextFieldの値変更

だけのシンプルな構造のメモアプリでざっくりと学んでみる。

Reducerの定義

struct MemoReducer: Reducer {
    struct State: Equatable {
        var items: [String] = []
        var text: String = ""
    }

    enum Action: Equatable {
        case addButtonTapped
        case deleteButtonTapped(index: Int)
        case textChanged(String)
    }

    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
        case .textChanged(let newText):
            state.text = newText
            return .none

        case .addButtonTapped:
            if !state.text.isEmpty {
                state.items.append(state.text)
                state.text = ""
            }
            return .none

        case .deleteButtonTapped(let index):
            guard state.items.indices.contains(index) else { return .none }
            state.items.remove(at: index)
            return .none
        }
    }
}

State の設計

  • items: メモリストのアイテムを格納する配列
  • text: テキストフィールドの現在の入力値

Action の定義

  • addButtonTapped: 追加ボタンがタップされた時
  • deleteButtonTapped(index: Int): 削除ボタンがタップされた時(削除するアイテムのインデックス付き)
  • textChanged(String): テキストフィールドの値が変更された時

Reducer ロジック

各Actionに対する状態変更のロジックをここで実装する。TCAでは状態の変更は必ずReducer内で行い、副作用が必要な場合はEffectを返す。今回は副作用がないため、すべて.noneを返すだけでOK。

Viewの実装

struct HomeView: View {
    let store: Store<MemoReducer.State, MemoReducer.Action>

    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            VStack {
                List {
                    ForEach(Array(viewStore.items.enumerated()), id: \.offset) { index, item in
                        HStack {
                            Text("🍎 \(item)")
                            Spacer()
                            Button("削除") {
                                viewStore.send(.deleteButtonTapped(index: index))
                            }
                            .foregroundColor(.red)
                        }
                    }
                }

                HStack {
                    TextField("新しいアイテム", text: viewStore.binding(
                        get: \.text,
                        send: TodoReducer.Action.textChanged
                    ))
                    .textFieldStyle(RoundedBorderTextFieldStyle())

                    Button("追加") {
                        viewStore.send(.addButtonTapped)
                    }
                }
                .padding()
            }
        }
    }
}

アプリのエントリーポイントの実装はこちら

import SwiftUI
import ComposableArchitecture

@main
struct SwiftUI_PlaygroundApp: App {
    // MARK: - Body
    var body: some Scene {
        WindowGroup {
            HomeView(
                store: Store(
                    initialState: TodoReducer.State(),
                    reducer: {
                        TodoReducer()
                    }
                )
            )
        }
    }
}

WithViewStore

TCAではWithViewStoreを使用してStoreから状態を観測し、Actionを送信する。observeパラメータで観測したい状態の部分を指定する。

データバインディング

テキストフィールドのバインディングはviewStore.bindingを使用して作成。getで現在の値を取得し、sendで値の変更をActionとして送信。

Actionの送信

ボタンタップなどのイベントはviewStore.send()メソッドを使用してActionを送信する。

TCA vs MVVM 比較表

項目TCAMVVM
学習コスト❌ 高い – 関数型プログラミングの知識が必要✅ 低い – 一般的なパターンで理解しやすい
状態管理✅ 予測可能 – 単方向データフローで状態変更が追跡しやすい❌ 複雑になりがち – 双方向バインディングで状態が散在
テスタビリティ✅ 優秀 – 純粋関数でテストが容易⚠️ 中程度 – ViewModelのテストは可能だが、UIとの結合度が高い
デバッグ✅ 容易 – すべてのActionとState変更をトレース可能❌ 困難 – 状態変更の追跡が難しい場合がある
コード量❌ 多い – ボイラープレートコードが多め✅ 少ない – シンプルな実装で済む
関心の分離✅ 明確 – UI、ロジック、副作用が完全に分離⚠️ 中程度 – ViewModelにロジックが集中しがち
パフォーマンス⚠️ 注意が必要 – 不適切な実装でパフォーマンス問題が発生する可能性✅ 良好 – 一般的に軽量
チーム開発✅ 適している – ルールが明確で一貫性を保ちやすい❌ ばらつきが生じやすい – 実装方法が開発者によって異なる
小規模アプリ❌ オーバーエンジニアリング – シンプルなアプリには重すぎる✅ 適している – 素早く開発できる
大規模アプリ✅ 優秀 – 複雑な状態管理も破綻しにくい❌ 破綻しやすい – 状態管理が複雑になりがち