導入
Swiftアプリケーション開発において、非同期APIリクエスト処理をクリーンでモジュール化された形で実装することは、アプリのスケーラビリティとメンテナンス性を高める上で非常に重要です。この記事では、TCA (The Composable Architecture) とSwift Concurrencyを組み合わせて、GitHub APIからデータを取得し、その結果をTCAのストアに反映する一連の流れを実現する方法について解説します。
実装手順
- TCAとSwift Concurrencyの概要
- APIクライアントの実装
- GitHubクライアントの依存関係注入
- レスポンス型の定義
- リクエストとレスポンスの実装
- リデューサーでのAPIリクエスト処理
- ビューでのデータ表示
- レスポンスヘッダーの処理
1. TCAとSwift Concurrencyの概要
TCA は、モジュール化された状態管理を提供し、アプリケーション全体の一貫性とテスト可能性を向上させるためのフレームワークです。一方、Swift Concurrency は、非同期プログラミングを簡潔かつエラー耐性のある形で実装するための最新のSwift機能です。
これらの技術を組み合わせることで、APIリクエスト処理をよりクリーンに、そして簡単に拡張可能な形で実装できます。
2. APIクライアントの実装
APIリクエストを処理するためのApiClient
を実装します。このクライアントは、非同期にAPIリクエストを送信し、結果をデコードして返す役割を持っています。
final class ApiClient: Sendable {
private let session: Session
init(session: Session) {
self.session = session
}
func send<T: BaseRequest>(request: T) async throws -> (T.Response, HTTPURLResponse) {
do {
let response = try await session.response(for: request)
guard let httpResponse = response.urlResponse as? HTTPURLResponse else {
throw ApiError.invalidResponse
}
return (response.object as! T.Response, httpResponse)
} catch let originalError as SessionTaskError {
switch originalError {
case let .connectionError(error), let .responseError(error), let .requestError(error):
throw ApiError.unknown(error as NSError)
case let .responseError(error as ApiError):
throw error
}
}
}
static let liveValue = ApiClient(session: Session.shared)
}
3. GitHubクライアントの依存関係注入
次に、GitHub APIに特化したクライアントを実装し、TCAのDependencyKey
を使用して依存関係を注入します。
import Dependencies
struct GithubClient: DependencyKey {
var searchRepos: @Sendable (String, Int) async throws -> (SearchReposResponse, HTTPURLResponse)
var searchFavorites: @Sendable () async throws -> (SearchFavoritesResponse, HTTPURLResponse)
static let liveValue: GithubClient = .live()
static func live(apiClient: ApiClient = .liveValue) -> Self {
.init(
searchRepos: { query, page in
try await apiClient.send(request: SearchReposRequest(query: query, page: page))
},
searchFavorites: {
try await apiClient.send(request: SearchFavoritesRequest())
}
)
}
}
4. レスポンス型の定義
APIのレスポンスをデコードするための構造体を定義します。これにより、APIから取得したデータを簡単に扱うことができます。
import Foundation
// SearchReposResponse.swift
struct SearchReposResponse: Decodable, Equatable {
let totalCount: Int
let items: [Item]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case items
}
struct Item: Decodable, Identifiable, Equatable {
let id: Int
let name: String
let fullName: String
let owner: Owner
let description: String?
let stargazersCount: Int
enum CodingKeys: String, CodingKey {
case id
case name
case fullName = "full_name"
case owner
case description
case stargazersCount = "stargazers_count"
}
}
struct Owner: Decodable, Equatable {
let login: String
let avatarUrl: URL
enum CodingKeys: String, CodingKey {
case login
case avatarUrl = "avatar_url"
}
}
}
// SearchFavoritesResponse.swift
struct SearchFavoritesResponse: Decodable, Equatable {
let items: [SearchFavoritesResponseItem]
struct SearchFavoritesResponseItem: Decodable, Identifiable, Equatable {
let id: Int
let name: String
let fullName: String
let owner: Owner
let description: String?
let stargazersCount: Int
enum CodingKeys: String, CodingKey {
case id
case name
case fullName = "full_name"
case owner
case description
case stargazersCount = "stargazers_count"
}
struct Owner: Decodable, Equatable {
let login: String
let avatarUrl: URL
enum CodingKeys: String, CodingKey {
case login
case avatarUrl = "avatar_url"
}
}
}
}
5. リクエストとレスポンスの実装
GitHub APIリクエストを実装するために、SearchReposRequest
とSearchFavoritesRequest
を作成します。
import APIKit
struct SearchReposRequest: GithubRequest {
typealias Response = SearchReposResponse
let query: String
let page: Int
var path: String { "/search/repositories" }
var method: HTTPMethod { .get }
var parameters: [String: Any]? {
[
"q": query,
"page": page
]
}
}
struct SearchFavoritesRequest: GithubRequest {
typealias Response = SearchFavoritesResponse
var path: String { "/user/starred" }
var method: HTTPMethod { .get }
}
6. リデューサーでのAPIリクエスト処理
GithubClient
を使用してリデューサーでAPIリクエストを処理し、取得したデータとレスポンスヘッダーをTCAのステートに反映させます。
import ComposableArchitecture
struct SearchReposState: Equatable {
var repos: [Repository] = []
var isLoading: Bool = false
var headers: [AnyHashable: Any] = [:]
}
enum SearchReposAction: Equatable {
case search(String)
case searchResponse(Result<([Repository], [AnyHashable: Any]), Error>)
}
struct SearchReposReducer: Reducer {
@Dependency(\.githubClient) var githubClient
func reduce(into state: inout SearchReposState, action: SearchReposAction) -> Effect<SearchReposAction> {
switch action {
case .search(let query):
state.isLoading = true
return .run { send in
await send(.searchResponse(Result {
let (response, headers) = try await githubClient.searchRepos(query, 1)
return (response.items.map(Repository.init), headers.allHeaderFields)
}))
}
case let .searchResponse(.success((repos, headers))):
state.repos = repos
state.headers = headers
state.isLoading = false
return .none
case .searchResponse(.failure):
state.isLoading = false
return .none
}
}
}
7. ビューでのデータ表示
取得したデータをビューに表示し、ユーザーに提供します。また、必要に応じてレスポンスヘッダーも表示できます。
import SwiftUI
import ComposableArchitecture
struct SearchReposView: View {
let store: Store<SearchReposState, SearchReposAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
TextField("Search...", text: .constant(""))
.onSubmit {
viewStore.send(.search("your-query"))
}
if viewStore.isLoading {
ProgressView("Loading...")
} else {
List(viewStore.repos) { repo in
Text(repo.name)
}
ForEach(viewStore.headers.keys.sorted(), id: \.self) { key in
if let value = viewStore.headers[key] as? String {
Text("\(key): \(value)")
}
}
}
}
}
}
}
まとめ
このサンプルコードでは、TCAとSwift Concurrencyを活用して、GitHub APIからデータを非同期で取得し、その結果をTCAのストアに反映する一連の流れを実現しました。依存関係の注入、非同期関数の呼び出し、APIクライアントの実装、リクエストのデコード、そしてTCAのエフェクトを組み合わせることで、クリーンでモジュール化されたAPIリクエスト処理が可能になっています。また、レスポンスヘッダーの処理を追加することで、さらに柔軟な対応ができるようになります。
このアプローチを使用することで、他のAPIエンドポイントにも簡単に拡張可能なシステムを構築できます。ぜひ、自身のプロジェクトにもこの手法を取り入れてみてください。
さいごに、年収4桁万円を達成中のiOSエンジニアが皆さんをお導きいたします!
ぜひメンターを受けてみてください〜
メンターはこちら
貴方もなれます!!
コメント