The Composable Architecture(TCA)の紹介と少し使った所感

# 概要

[The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture)という良さげなアーキテクチャがあると聞いて、iOSの簡単なアプリをSwiftUIで書いてみました。

---

# 作ったもの

https://github.com/tonionagauzzi/SwiftUITCASample
![todo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277107/2e6d4c9d-2aac-e468-9f70-6632649197fe.gif)

よくチュートリアルで作るTODOアプリです。

実は、Qiitaに[ほぼ同じことをされた先人さん](https://qiita.com/kalupas226/items/2c9680396b039fc7499b)がいました…（書く段階で知った。笑）
オリジナル性は無いですが、社内勉強会向けの資料なので、ご容赦ください！

---

# The Composable Architectureの説明

全体図

<img src=https://miro.medium.com/max/4800/1*cr6nuwGQp8iZrnvEEtLeZw.png />

> 出典：[The Composable Architecture — Visualize Data Flows With a Diagram](https://medium.com/swlh/the-composable-architecture-visualize-data-flows-with-a-diagram-817306831508)

---

各コンポーネントの説明をしていきます。

* State
* Action
* Environment
* Reducer
* Store
  * 先ほどの図でState、Action、Reducer、Effectを囲んでいるのがStoreです。
* Effect

---

## State
UIに表示する内容を定義します。

```Swift:ContentView.swift
// ToDo1個分のState。
struct ToDoState: Equatable, Identifiable {
    let id: UUID
    var description = ""
    var isCompleted = false
}

// アプリ全体のState。ToDoの配列。
struct AppState: Equatable {
    var todoStates: [ToDoState] = []
}
```

たとえば1つの画面でこれらを更新すると、他の画面にも即反映されます。

---

## Action
タップやデータ受信などのイベントを定義します。

```Swift:ContentView.swift
// ToDo1個に対して発生するイベント。
enum ToDoAction: Equatable {
    case checkTapped
    case textChanged(String)
    case removed
}

// アプリ全体に対して発生するイベント。TODOのは何番目に何を送るかを指定。
enum AppAction: Equatable {
    case todo(index: Int, action: ToDoAction)
    case addButtonTapped
}
```

Stateもそうですが、Equatableに準拠することでテストが容易になります（後述）。

---

## Environment

依存関係を外部から注入します。DIです。今回はあまり活用しませんでした。

```Swift:ContentView.swift
struct ToDoEnvironment {
}

struct AppEnvironment {
    var uuid: () -> UUID = UUID.init
}
```

DIとはなんぞやについては[別記事を参照](https://qiita.com/tonionagauzzi/items/87ecca0905face65babb)。Androidの記事ですが。

---

## Reducer

Actionを受けてStateを更新する役割です。

```Swift:ContentView.swift
let todoReducer = Reducer<ToDoState, ToDoAction, ToDoEnvironment> {
    state, action, environment in
    switch action {
    case .checkTapped:
        state.isCompleted.toggle()
        return .none
    case .textChanged(let text):
        state.description = text
        return .none
    case .removed:
        return .none
    }
}

let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
    todoReducer.forEach(
        state: \AppState.todoStates,
        action: /AppAction.todo(index:action:),
        environment: { _ in ToDoEnvironment() }
    ),
    Reducer { state, action, environment in
        switch action {
        case .todo(index: _, action: .checkTapped):
            state.todoStates = state.todoStates
                .enumerated()
                .sorted {
                    $0.element.description.lowercased()
                        < $1.element.description.lowercased()
                }
                .sorted {
                    !$0.element.isCompleted && $1.element.isCompleted
                }
                .map (\.element)
            return .none
        case .todo(index: let index, action: .removed):
            state.todoStates.remove(at: index)
            return .none
        case .todo(index: let index, action: let action):
            return .none
        case .addButtonTapped:
            state.todoStates.insert(
                ToDoState(id: environment.uuid()), at: state.todoStates.count
            )
            return .none
        }
    }
)
.debug()
```

ユーザー入力も処理結果も含めた全てのアクションがここへやって来ます。

---

## Store(ViewStore)

State、Action、そしてReducerの1セットを**Store**と呼びます。Viewを構築する役割です。

```Swift:ContentView.swift
let todoStore: Store<ToDoState, ToDoAction>

let appStore: Store<AppState, AppAction>
```

Storeは、Stateの変更を全て監視してViewを再レンダリングします。

また、Viewから発生したActionをReducerに渡す役割もあります。

---

## Effect

さて、今回は単純なサンプルなので、Reducerでは全部`return .none`していました。

しかし、本来Reducerは必要に応じて**Effect**を使います。

[こちらの例](https://github.com/pointfreeco/swift-composable-architecture#basic-usage)を使って説明します。

---

```Swift
struct AppEnvironment {
    var mainQueue: AnySchedulerOf<DispatchQueue>
    var numberFact: (Int) -> Effect<String, ApiError>
}
```

↑まず、Environmentの`numberFact`で時間のかかる処理をクロージャとして注入します。

処理はUIと非同期で実行したいので、実行スレッドの情報を`mainQueue`で渡します。

---

```Swift
let appView = AppView(
  store: Store(
    initialState: AppState(),
    reducer: appReducer,
    environment: AppEnvironment(
      mainQueue: .main,
      numberFact: { number in Effect(value: "\(number) is a good number Brent") }
    )
  )
)
```

↑これが渡し元です。アプリ全体のStore初期化時に、依存関係を注入しています。

---

```Swift
case .numberFactButtonTapped:
    return environment.numberFact(state.count)
        .receive(on: environment.mainQueue)
        .catchToEffect()
        .map(AppAction.numberFactResponse)
```

↑そしてReducerでは、受け取った処理を行いつつ、非同期に監視します。何かしらreceiveしたら、**Reducer自身に対して**新たなActionを発行します。

> [NOTE] ちなみに、returnの型は`Effect<AppAction, Never>`型となっています。

---

```Swift
case let .numberFactResponse(.success(fact)):
    state.numberFactAlert = fact
    return .none

case .numberFactResponse(.failure):
    state.numberFactAlert = "Could not load a number fact :("
    return .none
```


↑Reducerは新たなActionを受け取ったら、stateを更新します。成功/失敗の2種類あります。

---

このようにして、副作用と呼ばれるものを**外部に出さずReducer内で完結させられる**のが、Reduxなどには無いThe Composable Architectureの特徴です。

---

# TCAの強み

* State management
* Composition
* Side effects
* Testing
* Ergonomics

> 出典：[What is the Composable Architecture?](https://github.com/pointfreeco/swift-composable-architecture#what-is-the-composable-architecture)

---

一言で言えば、**画面をまたいで状態を値型で共有**でき、**小機能単位でStoreを分割**でき、**Actionの副作用をReducer内部で扱う**ことができ、**ビジネスロジックのテストが容易**で、これらすべてを**短く人間工学的なコードで書ける**、といったところでしょう。

---

## テストのしやすさ

ToDoリストに[削除機能を追加したコミット](https://github.com/tonionagauzzi/SwiftUITCASample/commit/dca044d2d03672755fe57a71a2569fbe41106118)です。

テストに注目します。

```Swift:SwiftUITCASampleTests.swift
    func testRemoveToDo() {
        let (uuid1, uuid2) = (UUID.init(), UUID.init())
        let store = TestStore(
            initialState: AppState(
                todoStates: [
                    ToDoState(
                        id: uuid1,
                        description: "ToDo 1",
                        isCompleted: false
                    ),
                    ToDoState(
                        id: uuid2,
                        description: "ToDo 2",
                        isCompleted: true
                    )
                ]
            ),
            reducer: appReducer,
            environment: AppEnvironment()
        )
        store.assert(
            .send(.todo(index: 0, action: .removed)) { expected in
                expected.todoStates = [
                    ToDoState(
                        id: uuid2,
                        description: "ToDo 2",
                        isCompleted: true
                    )
                ]
            }
        )
    }
```

---

初期`initialState`と、送る`action`とを指定し、実行後の`expected`が期待通りのStateであることを確認するテストですが、非常に読みやすく書けました。

また、サーバーやDBを使い分けるような大きなアプリになったとき、`MockEnvironment`みたいなダミーを`environment`に注入すれば、ビジネスロジックだけをテストすることも簡単です。

---

# 補足

Swift UIとCombineに依存しているため、iOS 13、macOS 10.15、Mac Catalyst 13、tvOS 13、および watchOS 6以上でなければ使えませんが、古いOS向けに[ReactiveSwift](https://github.com/trading-point/reactiveswift-composable-architecture)と[RxSwift](https://github.com/dannyhertz/rxswift-composable-architecture)のForkも一応用意されています。

---

# 所感

[クリーンアーキテクチャー](https://qiita.com/tonionagauzzi/items/8c9fb93520b56fd3a564)の記事を書いたのがちょうど2年前です。ってか、The Clean ArchitectureもTCAですね笑

今回のTCAを使ってみて、機能追加のしやすさやモジュール分割の自由度など拡張性の高さを感じました。この勢いで、もう少し複雑なアプリも作ってみたいですね。

---

一方、現状はApple製の新しめOS向けに特化したアーキテクチャなので、X-Platform前提の時点で難しかったり、古いOSのサポートを切れない実案件では選定しにくいという課題はあります。

実際、大規模案件では今でもMVVMやクリーンなほうのTCAを扱うことが多いです。

が、新しいアーキテクチャがあちこちで使われ出してるなと感じたら、基礎だけでも素早く学ぶよう個人的に心掛けています。

なぜかというと、人の書いたコードを読む抵抗が減るからです。

---

昨今、1から何かを発明する開発スタイルは淘汰され、既にあるものを繋ぎ合わせてニーズを実現するのが、いろんな分野で主流になって来てると感じます（※諸説あり）。

人の書いたコードやAPI仕様書を見たとき、いちいちアレルギーを発症していたらやっていけないので、「これはこういう思想でこうなんだな」とすぐに見抜くために、自分の中にパターン化された前提知識をいくつも持っていたいと思います。

と、良くない方向にポエム化して来たので、オチをつけましょう。最小機能単位で切り出しと疎結合ができるコンポーザブルアーキテクチャは、まさにこの切り貼りの時代にうってつけな感じですね！

---

# 参考記事

※もっとよく知りたい方は、これらの素晴らしい記事orセッションをおすすめします！

* [GitHub @pointfreeco/swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture)
* [Swiftによるアプリ開発のためのThe Composable Architectureがすごく良いので紹介したい](https://qiita.com/yimajo/items/77c204ab091223f9cb14)
* [the Composable Architecture の始め方](https://qiita.com/zeero/items/b77cb689d9a707d94ac7)
* [Composable Architecture を利用した Todo アプリの紹介（Part 1）](https://qiita.com/kalupas226/items/2c9680396b039fc7499b)
* [CA.swift #14 - モジュール分割した開発での知見とテーマ切り替えやUICatalogについて](https://www.youtube.com/watch?v=MQWW84lg9xc&t=2004s)