the Composable Architecture の始め方

:::note
* `2022/10/16 更新` v0.42.0から採用された[ReducerProtocol](https://github.com/pointfreeco/swift-composable-architecture/discussions/1282)で書き方が大きく変わりましたので、記載内容やコードを見直しました
:::

[iOSDCでのyimajoさんの発表](https://www.youtube.com/watch?v=bWR2vpcxzDA)など、[the Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture)（以下、TCA）が良さそうという評判を聞いて調べてみました。どこから始めていいのか少し迷ったので、公式レポジトリのREADMEにある[Basic Usage](https://github.com/pointfreeco/swift-composable-architecture#basic-usage)をベースにTCAの始め方を解説してみます。
TCAどころかSwiftUIすら勉強し始めなので、間違いなどあるかもしれません。コメントで教えて頂けると嬉しいです！

## TCAって何？

TCAはiOSなどのAppleプラットフォームのアプリケーション開発のためのフレームワークです。
Combineを前提としているため、iOSだとiOS13以上が対象となる制約があります。（ちなみにiOS13未満向けに[RxSwift版のfork](https://github.com/dannyhertz/rxswift-composable-architecture)もあるみたいです）
SwiftUIとの親和性が高く、SwiftUIをより使いやすくする機能が充実しています。

TCAが提供する機能については、READMEの[What is the Composable Architecture?](https://github.com/pointfreeco/swift-composable-architecture#what-is-the-composable-architecture)に以下が挙げられています。

* **State Management**
シンプルな値型によるアプリケーションの状態管理の手段を提供します。複数の画面にまたがって状態は共有され、1つの画面での状態の更新はただちに他の画面にも反映されます。

* **Composition**
巨大な機能を小さなコンポーネントにブレイクダウンして独立したモジュールに分離し、それらを簡単にまとめ直して1つの機能に組み上げる手段を提供します。

* **Side Effects**
可能な限り最もテスタビリティが高く、理解しやすい方法で、副作用を伴うアプリケーションの外側の世界とやり取りする手段を提供します。

* **Testing**
TCAを使って実装した機能をテストするだけでなく、多くの部品で構成された機能の統合テストを書いたり、副作用がアプリケーションにどのように影響するかを把握するためのエンドツーエンドのテストを書く手段を提供します。これにより、ビジネスロジックが期待通りに動作していることを強力に保証することができます。

* **Ergonomics**
コンセプトと部品を可能な限り少なくしたシンプルなAPIにより、上記のすべてを達成する手段を提供します。

## アーキテクチャ概要

コードを書き始める前に簡単にTCAのアーキテクチャとしての概要を説明したいと思います。

TCAはReduxやFluxといったアーキテクチャと近い構造を持っていて、Store、State、Action、Reducerという型を持っています。またこれらに加えてEffectという型も定義しています。

詳細は後回しにして、いったん図にまとめると以下のような関係性になります。
![TCAアーキテクチャ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36771/d816104c-da7c-4d68-d43d-ea0548ab0179.jpeg)
Viewでの操作（ユースケース）はすべてActionで表現します。
ActionをReducerで処理してStateに反映する単方向のデータ制御を実現しており、ViewStoreを経由してViewにStateの変更が反映されるようになっています。
データ層へのアクセスについては、ReducerからEffectを発行し、非同期でデータ操作を行います。結果はEffectからActionを投げることで再びReducerで処理され、Stateに反映されます。

では、各登場人物の詳細について解説します。まず、State/Action/Reducerです。

* **State**: 1つの機能がそのロジックを実行したり、UIを描画したりするために、必要となるデータを定義する。
* **Action**: 1つの機能で発生するすべてのアクション（ユーザからのUI操作やユーザへの通知やデータ層からのデータ受け取りなど）を表現する。
* **Reducer**: 受け取ったActionに応じてStateを更新するファンクション。Stateを更新するために発生するあらゆる作用（副作用を含む）に対して処理を行う責務をもち、作用からは `Effect` 型で値が返却されてくることになる。

上記で出てきたEffectがTCAに特有の考え方になっています。

* **Effect**
  * 副作用（Side Effect）を含む作用を表現する型
  * Reduxでは副作用はStore/State/Action/Reducerの枠組みの外で扱うべきとなっている（ActionCreatorなどが担当することになる）が、TCAではReducerの中で扱うことができるようになっている。

ReducerがEffectを扱うことができることによって、プレゼンテーション層/ドメイン層/データ層の各層で発生するすべてのアクションを単なるActionとして扱うことができるようになっていることが、TCAの特徴の一つだと思います。

最後にStoreです。

* **Store**: State/Action/Reducerを一つにまとめ、それらの窓口となる。すべてのActionはStoreに対して送信され、それを受けてStoreはReducerを動かす。Reducerの処理結果で発生したStateの変更は、Storeを経由してViewで監視できるようになっている。

StoreをViewにバインドする時は `WithViewStore` を使い、ViewStoreというViewにバインドできる型になります。

## Basic Usage

ここからはTCAを使ってコードを書いていきます。
[Basic Usage](https://github.com/pointfreeco/swift-composable-architecture#basic-usage)に記載されている、数字をインクリメント／デクリメントする画面を作成します。
<img width="320" alt="画面イメージ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36771/d7f91e55-e467-9fd7-bf05-79e77d00e6f5.png">
「+」ボタンでカウンタがインクリメントされ、「-」ボタンでデクリメントされます。
「Number fact」ボタンを押すとアラートを表示します。アラートではカウンタを含んだ文字列が表示されるようにします。

### プロジェクト作成からインストール

まずはXcodeで新規プロジェクトを作成します。「Interface」には「SwiftUI」を選択します。
<img width="640" alt="New Project" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36771/f4192108-da09-a598-b636-0de79426783b.png">
TCAをインストールします。
TCAはライブラリとして提供されており、Swift Package Managerを使ってインストールします。
<img width="640" alt="SPM" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36771/90313f03-5cbc-39d2-dbe3-366e88a68607.png">

### State/Action/Reducer

TCAの根幹となるState/Action/Reducerを作っていきますが、はじめにこれらをまとめるstructを作り、ReducerProtocolに準拠させます。これにより、State/Action/Reducerが関連付けされるようになります。

```swift
import Foundation
import ComposableArchitecture

struct Feature: ReducerProtocol {
}
```

`Feature` structのインナークラスとしてState/Action/Reducerを定義していきます。

#### State

```swift
import ComposableArchitecture

struct Feature: ReducerProtocol {
  struct State: Equatable {
    var count = 0
    var numberFactAlert: String?
  }
}
```

`count` が画面に表示するカウンタの値です。
`numberFactAlert` には画面の「Number fact」ボタンを押下した時にアラートで表示する文字列が格納されます。

#### Action

```swift
struct Feature: ReducerProtocol {
  struct State: Equatable { … }
  enum Action: Equatable {
    case factAlertDismissed
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse(TaskResult<String>)
  }
}
```

enumでActionを定義します。各ケースは以下のアクションに対応しています。

* `factAlertDismissed`: アラートのボタンを押した時のアクション
* `decrementButtonTapped`: マイナスボタンを押した時のアクション
* `incrementButtonTapped`: プラスボタンを押した時のアクション
* `numberFactButtonTapped`: 「Number fact」ボタンを押した時のアクション
* `numberFactResponse`: 「Number fact」ボタンにより発生するEffectの戻りのアクション

`factAlertDismissed` から `numberFactButtonTapped` までがプレゼンテーション層から発生するアクションです。
`numberFactResponse` はドメイン層/データ層から発生するアクションです。Basic Usageではデータ層へのアクセスはないため、ドメイン層からのアクションになりますが、データ層にアクセスした場合も似たような形になると思います。SwiftConcurrencyによる非同期処理の結果を受け取り、かつ、Swift標準のResultと同様に成功／失敗を表現できる `TaskResult` というTCA独自の型をassociated valueとして受け取れるようになっています。

#### Reducer

```swift
struct Feature: ReducerProtocol {
  struct State: Equatable { … }
  enum Action: Equatable { … }
  
  func reduce(into state: inout State, action: Action) -> Effect<Action, Never> {
    switch action {
      case .factAlertDismissed:
        state.numberFactAlert = nil
        return .none

      case .decrementButtonTapped:
        state.count -= 1
        return .none

      case .incrementButtonTapped:
        state.count += 1
        return .none

      case .numberFactButtonTapped:
        return .task { [count = state.count] in
          await .numberFactResponse(
            TaskResult {
              String(
                decoding: try await URLSession.shared
                  .data(from: URL(string: "http://numbersapi.com/\(count)/trivia")!).0,
                as: UTF8.self
              )
            }
          )
        }

      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への処理」と「実行するべきEffect」について記述します。
ケース文の最後にreturnしている `.none` はEmptyのEffectです。これによりそのケースでは実行されるべきEffectがないことを示しています。

Effectを使っている `case .numberFactButtonTapped:` について取り上げます。
「Number fact」ボタンが押された際は非同期処理を実行するようになっています。
returnしている `Effect.task` が`Task` のようにSwiftConcurrencyの非同期処理を動かせる機能を提供し、戻り値のStringをTaskResultとActionでラップしています。
出来上がったActionに応じて再度Reducerが呼び出され、ドメイン層/データ層からの戻り値を再びReducerで処理することができるようになっています。

（なお、この例では非同期処理にHTTP通信を使用しているため、正しく動作させるにはATSに例外設定する、といった対応が必要です（[設定方法の参考](https://developer.apple.com/jp/news/?id=jxky8h89)））

### View

SwiftUIのViewへTCAを組み込みます。プロジェクト新規作成時にデフォルトで存在する `ContentView` を使うように原本から少し改変しています。

```swift
import SwiftUI
import ComposableArchitecture

struct FeatureView: View {
  let store: StoreOf<Feature>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      VStack {
        HStack {
          Button("−") { viewStore.send(.decrementButtonTapped) }
          Text("\(viewStore.count)")
          Button("+") { viewStore.send(.incrementButtonTapped) }
        }

        Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
      }
      .alert(
        item: viewStore.binding(
          get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
          send: .factAlertDismissed
        ),
        content: { Alert(title: Text($0.title)) }
      )
    }
  }
}

struct FactAlert: Identifiable {
  var title: String
  var id: String { self.title }
}

struct FeatureView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView(
      store: Store(
        initialState: Feature.State(),
        reducer: reducer: Feature()
      )
    )
  }
}
```

通常のSwiftUIで出てこないTCA特有の要素について解説していきます。

#### `StoreOf<Feature>`

`ReducerProtocol` に準拠した　`Feature`　型をジェネリクスのパラメータに指定することで、簡単にこのビューで使うState/Action/Reducerを指定できるようになっています。

#### `WithViewStore(self.store) { viewStore in }`

`WithViewStore` でViewにStoreを組み込みます。
`WithViewStore` はSwiftUIの `View` を返すので、 `var body: some View {}` の中で使うことができます。

#### `Button("−") { viewStore.send(.decrementButtonTapped) }`

`viewStore.send()` でActionを送ります。
ここでは「-」ボタンを押した時に `.decrementButtonTapped` Actionを送るように設定しています。

#### `Text("\(viewStore.count)")`

`viewStore` のプロパティとしてStateのプロパティにアクセスできます。
Stateが更新されれば自動的にViewが更新されます。
SwiftUIだと値をバインドする時にはプロパティラッパーを使うことになると思うのですが、単なるViewStoreのプロパティとして書けるので簡単ですね。

#### `viewStore.binding(get:send:)`

`viewStore.binding(get:send:)` でSwiftUIの `Binding` を提供して、ViewとViewStoreの間に双方向のバインディングを実現します。ViewStore→View方向のバインディングは `get:` で指定してStateが引数として渡され、Viewへの出力内容を設定できます。View→ViewStore方向のバインディングは `send:` で指定し、Viewの操作をトリガーにActionを送ることができます。

ここでは `alert(item:content:)` の `item: Binding<FactAlert?>` を作るために使われています。
ViewStore→View方向では、Stateの `numberFactAlert` をAlertで使用する `FactAlert` struct に変換しています。Stateで保持しているドメイン層のモデルをプレゼンテーション層のモデル（ `FactAlert` ）に変換しているイメージかなと思います。
View→ViewStore方向では、アラートでボタンを押した時のアクションを設定しています。

#### `Store(initialState:reducer)`

`FeatureView_Previews` でプレビューできるようにStoreを作っています。

### App

最後にAppでの `FeatureView` 生成箇所を修正して完了です。

```swift
import SwiftUI
import ComposableArchitecture

@main
struct TCABasicUsageApp: App {
  var body: some Scene {
    FeatureView(
      store: Store(
        initialState: Feature.State(),
        reducer: Feature()
      )
    )
  }
}
```

やっていることはViewの `FeatureView_Previews` と同じです。

これで実行すれば画面が表示できると思います。お疲れ様でした！🎉

## 感想

TCA触ってみた感想です。

* 単方向制御がTCAの中で完結している
    * プレゼンテーション層からの入力も、副作用を伴うデータ層からの入力もActionに変換されるので単方向制御が実現しやすい。
        * ReducerからもEffectを介してActionを発行できるので、ドメイン層内のアクションにも対応できる。
    * 非同期の処理をEffectで表現して、CombineとState/Reducer/Actionの世界をうまく繋げている。
* SwiftUIとの親和性が高い
    * Viewでの値監視についてプロパティラッパーを意識しないで済むので$アクセスが不要になる。単なるViewStoreのプロパティとしてアクセスすればよくなるのでSwiftUIがより簡単になる。

煩雑になりがちな処理・データの流れが明快で、すごく洗練されたアーキテクチャになっているなと思いました。iOS13以上の制約がクリアできるならぜひ使いたいです。

あと、the Composable ArchitechtureのComposableたる所以であるState/Reducer/Actionをコンポーネント化・統合する機能についてはBasic Usageでは使われておらず、この記事では書いていません。時間があればまたどこかで書きたいなと思っています。

最後までご覧頂きありがとうございました🙇

## 参考リンク

* [Swiftによるアプリ開発のためのThe Composable Architectureがすごく良いので紹介したい - Qiita](https://qiita.com/yimajo/items/77c204ab091223f9cb14)
* [まだMVC,MVP,MVVMで消耗してるの？ iOS Clean Architectureについて - Qiita](https://qiita.com/koutalou/items/07a4f9cf51a2d13e4cdc)