本記事およびサンプルコードはTCA 1.5.0を使用しています
はじめに
TCAは注目度は高いと思っていますが、僕の(狭い)観測範囲では導入事例はまだあまり多くない印象です。
僕が関わっているプロダクトではガッツリTCAを使っているので、国内でもっともっとTCAが盛り上がってくれないと困る...!w
ということで、国内でのTCAの盛り上がりに少しでも貢献できればと思い、実践的なサンプルアプリケーションを作って解説記事を書いてみることにしました。
ちゃんと解説しようと思うと結構なボリュームになりそうなので、いくつかの記事に分けて公開していく予定です。
想定読者
TCAを全く知らない方向けの記事ではありません。
少なくとも公式チュートリアルは一通り目を通し、簡単にでも自分でサンプルアプリを作ったことがある方を想定しています。
簡単なサンプルアプリを作れるレベルから一歩先に進むために必要な技術要素を学べることをイメージしています。
サンプルアプリの概要
サンプルアプリのコードはGithubで公開しています。
モバイルアプリ開発者にはお馴染みの、Githubリポジトリ検索機能を持つアプリです。
以下の機能を備えています。
- 任意のクエリでリポジトリを検索できる
- 検索結果をページネートできる
- リポジトリ情報の詳細画面に遷移できる
- リポジトリをお気に入りに登録できる
- お気に入りで検索結果をフィルタできる
本サンプルアプリに取り入れている主なTCAの技術要素は以下の通りです。
特に、BindingsとDependenciesは公式チュートリアルには取り上げられていない内容なので、参考になれば幸いです。
実装解説
ここから実装の解説をしていきます。
解説を読みつつ手を動かすことで理解もより進むかなと思い、前述のGithubリポジトリにはスタータープロジェクトを用意しています。
スタータープロジェクトをベースにご自身で実装を追加し、動くアプリを実際に作ってみてください。
プロジェクト構成
最初に簡単にプロジェクト構成について触れておきます。
本プロジェクトはSPMを使ったマルチモジュール構成になっています。
SPMについて今は特に深く知っておく必要はありません。
とりあえず役割ごとにコードがモジュールに分かれていて、依存関係がどうなっているかをイメージできればOKです。
モジュール名 | 概要 |
---|---|
App | アプリのエントリポイント |
SearchRepositoriesFeature | Githubリポジトリ検索&一覧表示機能 |
RepositoryDetailFeature | Githubリポジトリの詳細情報表示機能 |
SharedModel | 各モジュール共通で使用するモデルを格納している |
GithubClient | Github APIにアクセスするクライアントのインタフェースを持つ |
GithubClientLive | Github APIにアクセスするクライアントの実装を持つ |
ApiClient | ApiClientを格納している |
モジュールと依存関係はPackage.swift
に全て記載されています。
例えば、SearchRepositoriesFeature
を見てみましょう。
dependencies
に依存先のモジュール名が書かれていて、SharedModel
、GithubClient
、RepositoryDetailFeature
の3つのモジュール、そしてライブラリのTCAに依存していることがわかります。
targets: [
...
.target(
name: "SearchRepositoriesFeature",
dependencies: [
"SharedModel",
"GithubClient",
"RepositoryDetailFeature",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
],
swiftSettings: [
.unsafeFlags([
"-strict-concurrency=complete"
])
]
),
...
]
なお、SPMによるマルチモジュール構成のプロジェクトを作成するにあたっては、以下の記事を参考にさせていただきました。
前述のスタータープロジェクトはすでにSPMでプロジェクトを構成しているので、Package.swift
にモジュールを追記していくことですぐにマルチモジュール構成のアプリを作ることができます。
依存の設計
まずはGithubClientのコードから見ていきます。
GithubClientはGithub APIにアクセスするクライアントで、リポジトリ検索を行うために使用します。
GithubClientはシンプルな構造体で実装されています。
DIをするにあたり、依存をインタフェースと実装に分けることはとても一般的です。
そして、iOSの世界ではそれをプロトコルによって実現するのが一般的かと思いますが、TCAの作者であるPoint-Freeの2人は、依存を構造体で設計することを推奨しています。
@DependencyClient
public struct GithubClient: Sendable {
public var searchRepos: @Sendable (_ query: String, _ page: Int) async throws -> SearchReposResponse
}
シンプルな構造体で依存を設計することによる最大のメリットは、様々なパターンのモック実装を簡単に作れることです。
例えば、空のレスポンスを返すモック実装、例外をスローするモック実装、アイテムを1つだけ含むレスポンスを返すモック実装、アイテムを100個含むレスポンスを返すモック実装...などです。
extension GithubClient {
static let live = Self(searchRepos: { try await ... }) // ライブ実装
static let empty = Self(searchRepos: { _ in .init(totalCount: 0, items: []) }) // 空のレスポンスを返す
static let fail = Self(searchRepos: { _ in NSError(domain: "", code: 0) }) // 例外をスローする
}
// ライブ実装をベースにカスタムしたりもできる
var client = GithubClient.liveValue
client.searchRepos = { _ in .init(totalCount: 0, items: []) }
プロトコルでこれをやろうとすると、パターンの数だけそのプロトコルに準拠する構造体なりクラスを実装することになります。
メソッドが1つだけであればまだ良いですが、メソッドが増えてくるとそのパターンは膨大になっていきます。
protocol GithubClientProtocol {
func searchRepos(query: String, page: Int) async throws -> SearchReposResponse
}
// ライブ実装
struct LiveGithubClient: GithubClientProtocol {
func searchRepos(query: String, page: Int) async throws -> SearchReposResponse {
try await ...
}
}
// 空のレスポンスを返す
struct EmptyGithubClient: GithubClientProtocol {
func searchRepos(query: String, page: Int) async throws -> SearchReposResponse {
.init(totalCount: 0, items: [])
}
}
// 例外をスローする
struct FailGithubClient: GithubClientProtocol {
func searchRepos(query: String, page: Int) async throws -> SearchReposResponse {
throw NSError(domain: "error", code: 0)
}
}
シンプルな構造体による設計は、Xcodeプレビューやテスト実装でそのパワーを発揮します。詳しくは後述します。
さて、GithubClientのコードをもう少し見ていきましょう。
以下のコードで、GithubClientをTestDependencyKey
に準拠させています。
extension GithubClient: TestDependencyKey {
public static let testValue = Self()
public static let previewValue = Self()
}
TestDependencyKey
はDependenciesというライブラリが提供するプロトコルです。
DependenciesはPoint-Free製の依存管理ライブラリで、SwiftUIのEnvironmentにインスパイアされて作成されました。
例えば、SwiftUIビューで現在適用されているColorSchemeの値を取り出したいとき、以下のようなコードを実装します。
これで変数colorScheme
に値が注入されます。
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme: ColorScheme
var body: some View { ... }
}
Dependenciesを利用すると、上記と同じような感覚でReducerにGithubClientのインスタンスをDIすることが可能になります。
public struct SearchRepositoriesReducer: Reducer {
...
@Dependency(\.githubClient) var githubClient
...
var body: some ReducerOf<Self> { ... }
}
DIできるようにするために、もう少し実装が必要です。
DependencyValues
にGithubClient型のcomputedプロパティを実装します。
public extension DependencyValues {
var githubClient: GithubClient {
get { self[GithubClient.self] }
set { self[GithubClient.self] = newValue }
}
}
ここまででGithubClientのインタフェースの実装は完了です。
次に、GithubClientのライブ実装のコードを見ていきます。
※ライブ実装という言葉がちょこちょこ出てきてますが、これは実際の動くアプリで使用する依存の実装(つまり実際にAPIなどにアクセスする)のことを指しています
GithubClientのライブ実装は、インタフェースとは別のモジュールで実装されています。
インタフェースはシンプルな構造体ですが、ライブ実装は多くの場合実装が複雑で、3rdパーティライブラリに依存していたりします。
このため、ライブ実装はインタフェースと比較してコンパイルに時間がかかります。
そこで、インタフェースとライブ実装を別々のモジュールに分けることが推奨されています。
インタフェースを軽くしておき、Featureモジュールをインタフェースだけに依存させておくことで、Xcodeプレビューが軽量になるなどのメリットがあります。
ライブ実装のモジュールでは、GithubClientをDependencyKey
に準拠させます。
DependencyKey
はTestDependencyKey
と対をなすプロトコルで、ライブ実装インスタンスをDependencyValues
に登録するために必要です。
以下のようにliveValue
プロパティを実装することで、アプリ実行時に@Dependency
を通じてライブ実装インスタンスがDIされます。
extension GithubClient: DependencyKey {
public static let liveValue: GithubClient = .live()
static func live(apiClient: ApiClient = .liveValue) -> Self {
.init(
searchRepos: {
try await apiClient.send(request: SearchReposRequest(query: $0.query, page: $0.page))
}
)
}
}
続く...
長くなってきたので、ここらで一旦区切りたいと思います。
次の記事ではリポジトリ検索画面の解説をする予定です。
お楽しみに!