Composable Architecture を利用した Todo アプリの紹介（Part2）

前回書いた [Composable Architecture を利用した Todo アプリの紹介（Part1）](https://qiita.com/kalupas226/items/2c9680396b039fc7499b)の続きで、 [Part2](https://www.pointfree.co/episodes/ep101-a-tour-of-the-composable-architecture-part-2#t549) の部分について書きました。

## 今回出来上がるもの
<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182606/de8f9da7-6960-f4af-88cd-23af66981ef7.gif">

## Composable Architecture を利用した Todo アプリ（Part2）
Part1 の最後まで終えた後の `Action` と `Reducer` は以下のようになっていると思います。

```swift:ContentView.swift
enum AppAction {
  case todoCheckboxTapped(index: Int)
  case todoTextFieldChanged(index: Int, text: String)
}

let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, _ in
  switch action {
  case let .todoCheckboxTapped(index: index):
    state.todos[index].isComplete.toggle()
    return .none

  case let .todoTextFieldChanged(index: index, text: text):
    state.todos[index].description = text
    return .none
  }
}
```
↑のままだと、`Action` を追加する度に `index` を必要とする `Action` が増えていき、`View` からの `index` のバインドも繰り返すことになってしまい非効率です。今は全ての `Todo` を対象にしてしまっているため、 `index` が必要となっていますが、一つの `Todo` のみを対象にした `Reducer` を作って、それを `Todo` のコレクションで動作する `Reducer` に変換できたら効率化できそうです。（`Todo` のコレクション一つ一つに対して、一つの `Todo` のみを対象にした `Reducer` を適用するイメージです）
Composable Architecture では、このような ユースケースをサポートするために `forEach` というオペレータがあります。これについては後で説明します。

### 単一の Todo のための Action, Environment, Reducer の定義
まずは、一つの `Todo` のみを対象とした `Reducer` を作る準備を進めるために、新しく以下のような `Action` を定義します。

```swift:ContentView.swift
enum TodoAction {
  case checkboxTapped
  case textFieldChanged(String)
}
```
`AppAction` にあったものから接頭辞である `todo` が切り取られ、`index` もなくなりました。

次に単一の `Todo` のための依存関係である `Environment` も定義します。特に今は依存関係は存在しないので、空で良いです。

```swift:ContentView.swift
struct TodoEnvironment {}
```

次に単一の `Todo` と `TodoAction` のみで動作する `Reducer` を定義します。

```swift:ContentView.swift
let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { state, action, _ in
  switch action {
  case .checkboxTapped:
    state.isComplete.toggle()
    return .none

  case .textFieldChanged(let text):
    state.description = text
    return .none
  }
}
```

### AppAction, appReducer をそれぞれ TodoAction, todoReducer と組み合わせて使用する

次に、`AppAction`を先ほど定義した特定の `TodoAction` で動作するように書き換えます。

```swift:ContentView.swift
enum AppAction {
  case todo(index: Int, action: TodoAction)
}
```

次に、一つの `Todo` に対して動作する `todoReducer` を、`Todo` のコレクション全体に対して動作する `Reducer` に変換するために、`forEach`を使用します。

```swift:ContentView.swift
let appReducer = todoReducer.forEach(
  state: <#T##WritableKeyPath<GlobalState, MutableCollection>#>,
  action: <#T##CasePath<GlobalAction, (Comparable, TodoAction)>#>,
  environment: <#T##(GlobalEnvironment) -> TodoEnvironment#>
)
```
だいぶ複雑なので、`forEach` が何をしようとしているかを説明していきます。
`forEach` には、`state`、`action`、`environment` の三つの引数があり、いくつかの Generics や `KeyPath`, `CasePath` などがあることがわかると思います。だいぶ複雑ではあるのですが、Composable Architecture では何度も使用することになるので、いずれ慣れるらしいです。（自分はまだ慣れていないですが...）

元の記事では `forEach` の中心的な考え方は、「ドメインの一部（今で言うと単一の `Todo` のみしか知らない `todoReducer` ）を、より複雑なドメイン（今で言うと `todo` のコレクションを知っている `appReducer` ）に変換したい」ということだと述べられています。
実際にそれを行っていくためには、単一の `Todo` の `state`、`action`、`environment` それぞれを適切に変換していく必要があります。

一つ一つの引数の埋め方について見ていきましょう。

まず、`state` です。

```swift
state: <#T##WritableKeyPath<GlobalState, MutableCollection>#>
```

`state` ではグローバルな `State` である `AppState` からコレクション（`Todos`）を取り出し、そのコレクション内の任意の要素を変更した上で、取り出したコレクション全体を `AppState` に戻すことができるようにする必要があります。`KeyPath` については詳しく説明しませんが（[こちらの記事](https://qiita.com/shiz/items/b49251001a9ea54e4beb)が参考になりました🙇‍♂️）、`WritableKeyPath` でこのようなことを実現することができます。

そのため、`WritableKeyPath` を利用して `state` の部分は以下のように書くことができます。

```swift
todoReducer.forEach(
  state: \AppState.todos,
  action: <#T##CasePath<GlobalAction, (Comparable, TodoAction)>#>,
  environment: <#T##(GlobalEnvironment) -> TodoEnvironment#>
)
```

次に、`action` です。

```swift
action: <#T##CasePath<GlobalAction, (Comparable, TodoAction)>#>
```

`action` では、グローバルな `Action` である `AppAction` の `case` を関心のある単一の `index` （コード上では `Comparable` ）と一緒になった `TodoAction` に分離する必要があります。ここではそのために、`CasePath` というものが使われています。`CasePath` は Composable Architecture の作者である Point-Free さんが作られているもので、struct に対して `KeyPath` を使うと struct のプロパティを分離できるのと似たように、enum に対して `CasePath` を使うと enum の `case` を分離することができます（[参照](https://github.com/pointfreeco/swift-case-paths)）。
以上を踏まえて、`action` は以下のように書くことができます。

```swift
todoReducer.forEach(
  state: \AppState.todos,
  action: /AppAction.todo(index:action:),
  environment: <#T##(GlobalEnvironment) -> TodoEnvironment#>
)
```

`action` の部分は、自分で頑張って書くこともできますが、Composable Architecture には変換値を生成できる機能があるため、`/AppAction.todo(index:action:)` のように書くことができています。

最後に、`environment` です。

```swift
environment: <#T##(GlobalEnvironment) -> TodoEnvironment#>
```

`environment` は `state` や `action` に比べると単純で、グローバルな `Environment` をローカルな `TodoEnvironment` に変換するだけです。今回はグローバルな `Environment` は特に必要ないため、省略することができ以下のように書くことができます（ついでに `state` の書き方も省略しています）。

```swift:ContentView.swift
let appReducer: Reducer<AppState, AppAction, AppEnvironment> = todoReducer.forEach(
  state: \.todos,
  action: /AppAction.todo(index:action:),
  environment: { _ in TodoEnvironment() }
)
```

もちろん、`todoReducer` がもっと複雑になれば、ここでグローバルな `Envrionment` を使うことも出てきますが今回は省略で OK です。

`forEach` 大変ですね...
実際大変だと思うのですが、`forEach` 内で面倒な `index` に関する作業を全て一箇所で処理してくれるため、`Reducer` 内では `index` について全く考える必要がなくなる利点があると作者は述べています。今はシンプルな `Todo` アプリであるため、その恩恵は感じることがあまりできませんが、`Action` の数が増えたり、`Effect` を実行する必要が出てきたりして、`todoReducer` がより複雑になるとこの恩恵を感じることができるようです。
実際、複雑には見えますが、やっていることは `appReducer` と `todoReducer` を繋ぐだけになっています。アーキテクチャによってはこのような部分を隠蔽するものもありますが、Composable Architecture でこのコードをコンパイルすることによって、正しくコードが動いていることを非常に強力に保証してくれるとのことです。また、（後の章にはなりますが）テストを書くとこの部分のコードは嫌でも実行されることになるため、テストカバレッジも得ることができるという仕組みになっているみたいです。

### View への適用

ここまでで、新たな `appReducer` の準備は整ったので、いよいよ View に適用していきます。
まず、Button の部分は ↓ のように書き換えることができます。

```swift:ContentView.swift
Button(action: { viewStore.send(.todo(index: index, action: .checkboxTapped)) }) {
```

次に、TextField の部分は ↓ のように書き換えられます。

```swift:ContentView.swift
TextField(
  "Untitled Todo",
  text: viewStore.binding(
    get: { $0.todos[index].description },
    send: { .todo(index: index, action: .textFieldChanged($0)) }
  )
)
```

この状態でビルドすれば、Part1 の時と同様に動作していることを確認することができます。`appReducer` と `todoReducer` を利用することによって、単一の `Todo` のための `todoReducer` を `Todo` のコレクションにうまく適用することができました 🎉

### View ヘルパー ForEachStore の利用

しかし、View の書き方はもっと改善することができます。

`ForEach` では直接 `ForEach` 内で `index` をいじり回す必要がありますが、Composable Architecture の View ヘルパーである `ForEachStore` を利用することでもっと綺麗に書くことができます。

`ForEachStore` には、`Action` が `index` と ローカルアクション（今でいうと `TodoAction`）である `Store` を渡してあげる必要があります。ちょうど View で宣言されている ↓ の `Store` はそのような構造になっていますね。

```swift
let store: Store<AppState, AppAction>
```

そのため、View 内で `ForEachStore` には以下のように渡してあげて、利用することができます。

```swift
ForEachStore(
  self.store
) { todoStore in

)
```

今渡している `store` は `AppState` 全てを保持していますが、今は `Todo` の State だけを保持する `store` を渡せれば良いです。また同様に `AppAction` の全ても保持していますが、`TodoAction` だけを知っている `store` を渡すことができれば良いです。Composable Architecture ではこのような変換をするための ↓ のように使うことができる `scope` というものがあります。

```swift
ForEachStore(
  self.store.scope(
    state: <#T##(AppState) -> LocalState#>,
    action: <#T##(LocalAction) -> AppAction#>
  )
) { todoStore in

)
```

`state` の部分ではグローバルな `AppState` をローカルな `Todo` に変換する必要がありますが、それは単純に ↓ のように実現することができます。

```swift
ForEachStore(
  self.store.scope(
    state: { $0.todos },
    action: <#T##(LocalAction) -> AppAction#>
  ),
  content: <#T##(Store<Identifiable, Action>) -> _#>
)
```

`action` の部分は ↓ のようになっていますね。

```swift
action: <#T##(LocalAction) -> AppAction#>
```

`state` はグローバルなものをローカルに変換していたのでスッと納得できましたが、`action` はローカルな `Action` （`TodoAction`）をグローバルな `AppAction` に変換しているので少し奇妙に思えますが、グローバルな `AppStore` をローカルな `TodoStore` に変換するために、`scope` ではローカルアクションをグローバルアクションに埋め込む方法を伝える必要があります。そのため、`action` では、ローカルからグローバルな `Action` に変換するクロージャを引数にとっています（[参照](https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Store.swift)）。

これで目的の `TodoStore` を手に入れることができたので、あとは `index` を気にせず、 `WithViewStore` を用いて View を構築していくことになります。
全体像は ↓ のようになるかと思います。

```swift:ContentView.swift
struct ContentView: View {
    let store: Store<AppState, AppAction>
    
    var body: some View {
        NavigationView {
            WithViewStore(self.store) { viewStore in
                List {
                    ForEachStore(
                        self.store.scope(state: \.todos, action: AppAction.todo(index:action:))
                    ) { todoStore in
                        WithViewStore(todoStore) { todoViewStore in
                            HStack {
                                Button(action: { todoViewStore.send(.checkboxTapped) }) {
                                    Image(systemName: todoViewStore.isComplete ? "checkmark.square" : "square")
                                }
                                .buttonStyle(PlainButtonStyle())
                                
                                TextField(
                                    "Untitled Todo",
                                    text: todoViewStore.binding(
                                        get: \.description,
                                        send: TodoAction.textFieldChanged
                                    )
                                )
                            }
                            .foregroundColor(todoViewStore.isComplete ? .gray : nil)
                        }
                    }
                }
            }
        }
    }
}
```

↑ だと View が大きすぎるので、View を分割しましょう。
`todoStore` を利用している `WithViewStore` をちょうど分割することができそうなので、↓ のように分割します。

```swift:ContentView.swift
struct TodoView: View {
  let store: Store<Todo, TodoAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      HStack {
        Button(action: { viewStore.send(.checkboxTapped) }) {
          Image(systemName: viewStore.isComplete ? "checkmark.square" : "square")
        }
        .buttonStyle(PlainButtonStyle())

        TextField(
          "Untitled Todo",
          text: viewStore.binding(
            get: { $0.description },
            send: { .textFieldChanged($0) }
          )
        )
      }
      .foregroundColor(viewStore.isComplete ? .gray : nil)
    }
  }
}
```

この `TodoView` は `ForEachStore` で ↓ のように `content` の引数に取って扱うことができます。

```swift:ContentView.swift
struct ContentView: View {
    let store: Store<AppState, AppAction>
    
    var body: some View {
        NavigationView {
            WithViewStore(self.store) { viewStore in
                List {
                    ForEachStore(
                        self.store.scope(state: \.todos, action: AppAction.todo(index:action:)),
                        content: TodoView.init(store:)
                    )
                }
            }
        }
    }
}
```

これで、全く同じものを綺麗な状態のコードで作り上げることができました。

### Todo を増やす機能の追加

`Reducer` と View が綺麗になったので、機能を追加します。
現状は元々存在している `Todo` が固定のものとなっており、`Todo` をそれ以上増やすことができないため、`Todo` 追加機能を実装します。

まずは `NavigationBar` に `Todo` 追加用のボタンを実装します。

```swift:ContentView.swift
.navigationBarItems(trailing: Button("Add") {})
```

`Add` ボタンを押した時に `Action` を発火させたいので、`AppAction` を追加しましょう。

```swift:ContentView.swift
enum AppAction {
  case addButtonTapped
  case todo(index: Int, action: TodoAction)
}
```

もちろん `Action` は `Reducer` で処理してあげる必要があります。
現在の `Reducer` は ↓ のようになっています。

```swift:ContentView.swift
let appReducer: Reducer<AppState, AppAction, AppEnvironment> = todoReducer.forEach(
  state: \.todos,
  action: /AppAction.todo(index:action:),
  environment: { _ in TodoEnvironment() }
)
```

このままだと、さらに `AppAction` を扱うことができません。
扱うためには、新しい `Reducer` を作成し、それを `todoReducer` と組み合わせるようにします。その際には `combine` というものを以下のように使うことができます。

```swift:ContentView.swift
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
  todoReducer.forEach(
    state: \.todos,
    action: /AppAction.todo(index:action:),
    environment: { _ in TodoEnvironment() }
  ),
  Reducer { state, action, environment in
    // ...
  }
)
```

`combine` は同じドメインで動作している複数の `Reducer` を引数に取ります。そしてその複数の `Reducer` のリストを反復してそれぞれの `Reducer` を実行し、最終的に `Reducer` で返却される全ての `Effect` をマージして一つの `Reducer` に結合します。
これに関しては、言葉の説明よりも `combine` のコードを見た方がイメージがつきそうなので ↓ にも貼っておこうと思います。

```swift:ComposableArchitecture/Reducer.swift
  public static func combine(_ reducers: [Reducer]) -> Reducer {
    Self { value, action, environment in
      .merge(reducers.map { $0.reducer(&value, action, environment) })
    }
  }
```

`merge` は `Effect` の中で定義されていますが ↓ のようになっています。自分は Combine をあまり触ったことがないため、MergeMany も初めてみましたが個人的には[こちら](https://medium.com/@leonardo.dematossouza/using-combines-mergemany-to-fulfill-your-requests-99e652b89cbf)が参考になりました。

```swift:ComposableArchitecture/Effect.swift

  public static func merge<S: Sequence>(_ effects: S) -> Effect where S.Element == Effect {
    Publishers.MergeMany(effects).eraseToEffect()
  }
```

話が少し逸れてしまいましたが、新しく定義した `Reducer` の中で `Action` を処理していきましょう。

```swift:ContentView.swift
Reducer { state, action, _ in
  switch action {
  case .addButtonTapped:
    state.todos.insert(Todo(id: UUID()), at: 0)
    return .none

  case .todo(index: _, action: _):
    return .none

  }
}
```

`Todo` それぞれはユニークなものにしたいため、`id` は `UUID` で宣言するようにしています。

最後に `Action` を View から送るようにすれば `Todo` を追加できるようになったアプリが出来上がりです 🍵

```swift:ContentView.swift
.navigationBarItems(trailing: Button("Add") {
  viewStore.send(.addButtonTapped)
})
```

## おわりに
前回同様ほぼ翻訳のようになってしまいましたが、ある程度理解できてすっきりしてきました。
次の [Part3](https://www.pointfree.co/episodes/ep102-a-tour-of-the-composable-architecture-part-3) は Composable Architecture の便利なテスト機能について紹介されています。
また気力があれば、そちらの紹介もできたらと思います！