🍆
Target 'CasePathsMacros' must be enabled fefore it can ve used.
TCA久しぶりに使うとエラーが出た?
久しぶりにTCAを使ったみようとしたらエラーが出た?
SPMを使用してパッケージを追加する。


errorが出ている箇所を削除すればビルドすることはできた。


Todoアプリを作ってみた。
メモリに値を保存するだけなのでデータの永続化はできません。画面が切り替わる状態を管理するだけの単純なアプリです。単純だけど作るのが難しかった。

Todoの値を保持するモデルを作成。
Models/TodoItem.swift
// TodoItem: 個々のTodoアイテムを表すモデル
// - Equatable: 値の比較が必要なため
// - Identifiable: SwiftUIのForEachで使用するため
import Foundation
struct TodoItem: Equatable, Identifiable {
let id: UUID // Todoの一意識別子
var title: String // Todoのタイトル
var isCompleted: Bool // 完了状態のフラグ
init(id: UUID = UUID(), title: String, isCompleted: Bool = false) {
self.id = id
self.title = title
self.isCompleted = isCompleted
}
}
Reduxの経験がある方は見てみるとわかるかもしれませんが、ActionとReducerを作成します。
Features/TodoFeature.swift
// TodoFeature: TCAのメイン機能を実装
// アプリケーションの状態管理、アクション、および状態更新ロジックを定義
import Foundation
import ComposableArchitecture
@Reducer
struct TodoFeature {
// State: アプリケーションの状態を定義
struct State: Equatable {
var todos: IdentifiedArrayOf<TodoItem> = [] // Todoリストの配列
var newTodoTitle: String = "" // 新規Todo入力用の文字列
var filter: Filter = .all // 現在のフィルター状態
// フィルターの種類を定義
enum Filter {
case all // すべてのTodo
case active // 未完了のTodoのみ
case completed // 完了済みのTodoのみ
}
// 現在のフィルター状態に応じてTodoをフィルタリング
var filteredTodos: IdentifiedArrayOf<TodoItem> {
switch filter {
case .all:
return todos
case .active:
return todos.filter { !$0.isCompleted }
case .completed:
return todos.filter { $0.isCompleted }
}
}
}
// Action: アプリケーションで発生する可能性のあるすべてのアクションを定義
enum Action {
case addTodoButtonTapped // 追加ボタンが押された
case todoTitleChanged(String) // Todo入力テキストが変更された
case todoToggled(TodoItem.ID) // Todoの完了状態が切り替えられた
case todoDeleted(TodoItem.ID) // Todoが削除された
case filterSelected(State.Filter) // フィルターが選択された
}
// Reducer: 各アクションに対する状態更新ロジックを実装
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .addTodoButtonTapped:
guard !state.newTodoTitle.isEmpty else { return .none }
state.todos.append(TodoItem(title: state.newTodoTitle))
state.newTodoTitle = ""
return .none
case let .todoTitleChanged(title):
state.newTodoTitle = title
return .none
case let .todoToggled(id):
guard let index = state.todos.index(id: id) else { return .none }
state.todos[index].isCompleted.toggle()
return .none
case let .todoDeleted(id):
state.todos.remove(id: id)
return .none
case let .filterSelected(filter):
state.filter = filter
return .none
}
}
}
}
TCAの操作をするViewを作成。今回は、追加・表示・チェック・削除を実装しております。Storeを使ってStateの監視をしております。Reactだったらボタンのコールバック{}の中に、関数を呼び出すディスパッチがあります。TCAがやってくれていることは、画面が切り替わるかどうかの状態を監視と状態の更新をしてくれていることです。これだけですね。
Views/TodoListView.swift
// TodoListView: メインのTodoリストビュー
// TCAのStoreを受け取り、UIとの連携を行う
import SwiftUI
import ComposableArchitecture
struct TodoListView: View {
let store: StoreOf<TodoFeature> // TCAのストアを保持
var body: some View {
// WithViewStore: Storeの状態を監視し、アクションを送信するためのラッパー
WithViewStore(store, observe: { $0 }) { viewStore in
NavigationView {
VStack {
// 新規Todo入力フィールド
HStack {
// 双方向バインディング: テキスト入力とState更新
TextField("新しいTodoを入力", text: viewStore.binding(
get: \.newTodoTitle,
send: TodoFeature.Action.todoTitleChanged
))
.textFieldStyle(RoundedBorderTextFieldStyle())
// 追加ボタン: 空の入力を防ぐ
Button("追加") {
viewStore.send(.addTodoButtonTapped)
}
.disabled(viewStore.newTodoTitle.isEmpty)
}
.padding()
// フィルターセグメントコントロール
// 双方向バインディング: フィルター選択とState更新
Picker("フィルター", selection: viewStore.binding(
get: \.filter,
send: TodoFeature.Action.filterSelected
)) {
Text("全て").tag(TodoFeature.State.Filter.all)
Text("未完了").tag(TodoFeature.State.Filter.active)
Text("完了").tag(TodoFeature.State.Filter.completed)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
// Todoリスト表示
List {
ForEach(viewStore.filteredTodos) { todo in
HStack {
// 完了状態トグルボタン
Button(action: {
viewStore.send(.todoToggled(todo.id))
}) {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
}
.buttonStyle(PlainButtonStyle())
// Todoタイトル(完了時は取り消し線を表示)
Text(todo.title)
.strikethrough(todo.isCompleted)
Spacer()
// 削除ボタン
Button(action: {
viewStore.send(.todoDeleted(todo.id))
}) {
Image(systemName: "trash")
.foregroundColor(.red)
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
.navigationTitle("TCA Todo Demo")
}
}
}
}
アプリのエントリーポイントのファイルでTCAを設定すればデモ用のTODOアプリを使うことができます。
// TcaDataFlowDemoApp: アプリケーションのエントリーポイント
// TCAのストアを初期化し、アプリケーション全体で共有する
import SwiftUI
import ComposableArchitecture
@main
struct TcaDataFlowDemoApp: App {
// グローバルなTCAストアを静的プロパティとして保持
// - initialState: 初期状態を設定
// - reducer: 状態更新ロジックを提供
static let store = Store(initialState: TodoFeature.State()) {
TodoFeature()
}
var body: some Scene {
WindowGroup {
// ルートビューにストアを注入
TodoListView(store: Self.store)
}
}
}
まとめ
個人開発や仕事では使ったことがなくて興味がありたまに使ってみるとエラーが詰まることが多いのですが、SwiftUIはMVVMが相性が悪いらしくTCAで開発するのが流行りのようです。
これはAIに質問してみた内容です。
SwiftUIとMVVMの相性が悪い主な理由:
- @State/@StateObjectの存在
- SwiftUIは独自の状態管理システムを持つ
- MVVMのViewModelとの役割重複が発生
- 宣言的UI
- SwiftUIは状態変更を自動的にUIに反映
- MVVMのバインディング層が不要になる
- データフロー
- SwiftUIはデータの単方向フロー
- MVVMの双方向バインディングと競合
より適したパターン:
// シンプルなデータフローの例
struct ContentView: View {
@StateObject private var state = ViewState()
var body: some View {
List(state.items) { item in
ItemRow(item: item)
}
}
}
参考になった記事も貼っておきます
Discussion