TCAでGithubリポジトリ検索アプリを作ってみよう②

:::note warn
本記事およびサンプルコードはTCA 1.5.0を使用しています
:::

# はじめに
[TCAでGithubリポジトリ検索アプリを作ってみよう①](https://qiita.com/takehilo/items/814319d4666fef402a41)の続きです。

https://qiita.com/takehilo/items/814319d4666fef402a41

前回はプロジェクト構成やAPIクライアントの設計の話まででTCAについては特に触れてませんでしたが、今回はいよいよTCAで作った画面の実装の解説をします。

<br>

ちなみに、この記事は [NewsPicks アドベントカレンダー 2023](https://qiita.com/advent-calendar/2023/newspicks) の11日目の記事です。
NewsPicksの他の技術記事も面白いので是非読んでみてください！

https://qiita.com/advent-calendar/2023/newspicks

# リポジトリリストの表示
最初に、リポジトリリストの表示機能について見ていきます。

リポジトリリスト表示機能は、リスト表示に関するロジックを持つSearchRepositoriesReducerと、リストの各行（アイテム）に関するロジックを持つSearchRepositoriesItemReducerで構成されています。
親であるSearchRepositoriesReducerに対して、子であるSearchRepositoriesItemReducerが複数存在するという関係です。

これをStateで表現すると以下のようになります。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    public struct State: Equatable {
        var items = IdentifiedArrayOf<RepositoryItemReducer.State>()
    }

    // ...
}
```

IdentifiedArrayは**Arrayよりも効率的かつ安全にアイテムのコレクションを扱える**ように設計されたAPIです。
TCAにおいてリストのアイテムをモデリングするときは基本的にIdentifiedArrayを使うことになるでしょう。
※`IdentifiedArrayOf<Element>`は`IdentifiedArray<Element.ID, Element>`のタイプエイリアスです。

子Reducerを親Reducerに統合するため、親Reducerに以下の実装を加えます。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    // ...

    public enum Action: Sendable {
        case items(IdentifiedActionOf<RepositoryItemReducer>)
    }

    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            // ...
        }
        .forEach(\.items, action: \.items) {
            RepositoryItemReducer()
        }
    }
}
```

<br>

次に、ビューの実装を見ていきます。
SwiftUI.Listを使ってリストを表示します。

ForEachStoreはTCAが提供するSwiftUI.ForEachのラッパーAPIで、アイテムのドメインにスコープしたStore（`StoreOf<RepositoryItemReducer>`）を生成してくれます。
生成したStoreをリストアイテムのビューであるRepositoryItemViewに渡します。

```swift
public struct SearchRepositoriesView: View {
    WithViewStore(store, observe: { $0 }) { viewStore in
        List {
            ForEachStore(store.scope(
                state: \.items,
                action: \.items
            )) { itemStore in
                RepositoryItemView(store: itemStore)
            }
        }
    }
}
```

リスト表示部分だけを抜き出すとこんな感じです。

# 検索クエリを入力してGithubAPIにリクエストする
本サンプルアプリは任意の検索クエリを入力してGuthubリポジトリを検索できる機能を持っています。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81715/83a05ede-52bb-33ad-2802-ab056b4c5fd5.gif">

検索バーに文字を入力すると、SwiftUIのBindingを通じてアクションが送信され、Reducerは入力されたクエリを使ってGithub APIにリクエストを行います。

検索バーはSwiftUIの`searchable(text:placement:prompt:)`モディファイアを使って表示します。
NavigationStackStoreについてはいずれ触れますが、SwiftUIのNavigationStackを内部で実装している、TCAが提供するビューです。
NavigationStack内のビューに`searchable(text:placement:prompt:)`モディファイアを追加することで、検索バーが自動で追加されます。

```swift
NavigationStackStore(store.scope(state: \.path, action: \.path)) {
    WithViewStore(store, observe: { $0 }) { viewStore in
        List {
            // ...
        }
        .searchable(text: viewStore.$query)
    }
}
```

`searchable(text:placement:prompt:)`モディファイアにはBindingを渡しますが、ViewStoreからBindingを派生する方法には2種類あります。

1. ViewStoreの`binding(get:send:)`メソッドを使う
2. `@BindingState`を使う

1の`binding(get:send:)`メソッドを使う方法は、ボイラープレートが多く冗長になりがちです。
具体的なコードで示しましょう。
まず、Stateに検索クエリ文字列を管理する`query`プロパティを定義します。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    public struct State: Equatable {
        // ...
        var query = ""  // 追加
    }
}
```

TCAにおいては、Stateはアクションを通じてしか更新することができません。
Bindingでもそれは同じで、`query`プロパティを更新するためのアクションが必要です。
ここでは`queryChanged(String)`というアクションを実装しましょう。

Reducerは`queryChanged(String)`アクションを受け取ったらStateを更新します。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    // ...
    public enum Action: Sendable {
        // ...
        case queryChanged(String)  // 追加
    }

    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case let .queryChanged(newValue):
                state.query = newValue  // queryを更新
                return .none
            }
        }
    }
}
```

ビューでは、`binding(get:send:)`メソッドを使ってBindingを作成します。

```swift
.searchable(text: viewStore.binding(get: \.query, send: { .queryChanged($0) }))
```

`query`プロパティを更新するためだけに、ActionとReducerの単純なコードの追加が必要です。
プロパティが一つだけだと気になりませんが、数が増えてくるとボイラープレートが増え、Reducerのコードが冗長になります。

そこで2の`@BindingState`の出番です。
`@BindingState`はまさにこのボイラープレートを無くすために登場しました。

まず、`query`プロパティに`@BindingState`を追加します。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    public struct State: Equatable {
        // ...
        @BindingState var query = ""  // @BindingStateを追加
    }
}
```

次に、ActionをBindableActionプロトコルに準拠させます。
このプロトコルに準拠するためには`binding(BindingAction<State>)`というケースの追加が必要です。

そして、bodyにBindingReducerを追加します。
BindingReducerはBindingを通じて送信されたアクションを受信し、`@BindingState`がついたプロパティの値を更新してくれます。
これによってプロパティ更新のコードを自分で書く必要がなくなります。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    // ...
    public enum Action: BindableAction, Sendable {  // BindableActionに準拠
        // ...
        case binding(BindingAction<State>)  // 追加
    }

    public var body: some ReducerOf<Self> {
        BindingReducer()  // 追加
        Reduce { state, action in
            switch action {
            // ...
            }
        }
    }
}
```

ビューでは以下のように`$`付きでプロパティを参照することで、ビューにBindingを渡すことが出来ます。

```swift
.searchable(text: viewStore.$query)
```

ここまでで、検索バーに検索クエリを入力することでStateを更新できるようになりました。
次は、検索クエリの入力を検知してGithub APIにリクエストを送信する部分を見ていきます。

以下のように実装すると、検索クエリが入力された直後に何か処理を行うことが出来ます。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    // ...
    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .binding(\.$query):
                // ...検索クエリが入力されたときに実行されるロジック
            }
        }
    }
}
```

検索バーに文字を入力するごとにGithub APIにリクエストを送信するコードは以下のようになります。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    // ...
    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .binding(\.$query):
                // 検索クエリが入力されるたびにここが実行される
                return .run { [query = state.query, page = state.currentPage] send in
                    await send(.searchReposResponse(Result {
                        try await githubClient.searchRepos(query: query, page: page)
                    }))
                }
            }
        }
    }
}
```

## Debounce
検索バーに1文字追加するごとに検索処理が行われるようなUIはかえって鬱陶しいです。
ユーザが検索したい文言を入力し終わるのを一定時間待つことができると、使いやすいUIになります。
このような振る舞いをDebounceと言います。

Debounceを実装するのは非常に簡単です。
まず、Reducerに`mainQueue`をDIします。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    // ...
    @Dependency(\.mainQueue) var mainQueue
}
```

`mainQueue`の実体は`AnySchedulerOf<DispatchQueue>.main`で、簡単に言うとメインスレッド上で処理の実行タイミングを制御できるAPIです。
Debounceだったり、一定間隔で何か処理をするようなタイマー的な使い方をすることもできます。

Debounceの実装は、以下のようにEffectの`debounce(id:for:scheduler:options)`メソッドを呼び出してあげるだけです。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    // ...

    // 追加
    enum CancelId { case searchRepos }
    
    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .binding(\.$query):
                return .run { [query = state.query, page = state.currentPage] send in
                    await send(.searchReposResponse(Result {
                        try await githubClient.searchRepos(query: query, page: page)
                    }))
                }
                // 追加
                .debounce(id: CancelId.searchRepos, for: 0.3, scheduler: mainQueue)
            }
        }
    }
}
```

`0.3`を指定していますが、これは「検索バーに文字が入力されてから0.3秒間次の文字の入力がなかったらGithub APIにリクエストを送信する」という意味になります。
なお、すでに実行中のEffect（APIリクエスト）があったらそれをキャンセルし、新しいEffectを実行します。

`CancelId.searchRepos`は、Effectをキャンセルするときに対象のEffectを特定するために使用されるIDです。
IDはHashableに準拠している必要があり、上記のようにenumで実装する方法がTCAのドキュメントで示されています。

# ビューの無駄な再描画を防ぐ
リスト表示部分のコードを改めて見てみましょう。

```swift
public struct SearchRepositoriesView: View {
    WithViewStore(store, observe: { $0 }) { viewStore in
        List {
            ForEachStore(store.scope(
                state: \.items,
                action: \.items
            )) { itemStore in
                RepositoryItemView(store: itemStore)
            }
        }
    }
}
```

WithViewStoreが以下のように実装されています。

```swift
WithViewStore(store, observe: { $0 }) { viewStore in
    // ...
}
```

引数`observe`に`{ $0 }`が渡されていますが、これはSearchRepositories.Stateの全てのプロパティの変更を検知してビューを再描画するという指定になります。
SwiftUIにおいてビューの再描画はパフォーマンス悪化につながるため、なるべく避けなければなりません。

例えば本サンプルアプリでは、リストを下にスクロールしていくと、リストの最後のアイテムが表示されたタイミングでページング処理が実行されます。
具体的には、アイテムが表示されたというアクションが送信され、それがリストの最後のアイテムかつ次のページが存在するという条件を満たすと、Github APIに次のページをリクエストします。
このとき、ページング処理の状態を表すStateのプロパティが更新されます。

ビューでStateの全てのプロパティを監視していると、ページング処理が実行されるときにStateが更新されるためビュー全体が再描画されます。
スクロール操作中にビュー全体が再描画されることになるので、スクロール操作がカクついてしまいます。

そこで、ビューに必要なプロパティのみを保持するViewStateという構造体を定義します。

```swift
public struct SearchRepositoriesView: View {
    // ...

    // 追加
    struct ViewState: Equatable {
        @BindingViewState var query: String
    }

    // 追加
    init(store: BindingViewStore<SearchRepositoriesReducer.State>) {
        self._query = store.$query
    }

    // 監視対象をStateからViewStateに変換
    WithViewStore(store, observe: ViewState.init) { viewStore in
        // ...
    }
}   
```

上記の例では、SearchRepositoriesViewは検索クエリが更新されるときに限って再描画を行います。
ページング処理でStateが更新されてもビューの再描画は行われないので、スクロールパフォーマンスに影響することがありません。

このテクニックは[TCAの公式ドキュメント](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance#View-stores)でも紹介されています。

上記の例では、`@BindingViewState`と`BindingViewStore`という見慣れない型が使われています。
これらは`@BindingState`がついたStateのプロパティをViewStateで扱うときに必要なります。

SearchRepositoriesReducer.Stateの`query`プロパティには`@BindingState`をつけていましたね。
ViewStateでこのプロパティを扱いたいときに上記のような書き方をする必要があります。

```swift
@Reducer
public struct SearchRepositoriesReducer: Reducer, Sendable {
    public struct State: Equatable {
        // ...
        @BindingState var query = ""
    }
}
```

# 続く...
検索バーに検索クエリを入力し、リポジトリのリストを表示する部分の実装について解説しました。

次の記事ではXcode Previewsについて解説する予定です。
お楽しみに！