こんにちは、FAANS部の中島 (@burita083) です。2021年10月に中途入社し、FAANSのiOSアプリの開発を行なっています。
FAANSの由来は「Fashion Advisors are Neighbors」で、「ショップスタッフの効率的な販売をサポートするショップスタッフ専用ツール」です。現在正式リリースに向けて、WEARと連携したコーディネート投稿機能やその成果を確認できる機能など開発中です。
はじめに
FAANS iOSでは非同期処理にCombineを利用しています。Combine自体は本記事では詳しく解説をしませんが、RxSwiftを利用したことがある方なら特に違和感なく使えるかと思います。全く馴染みがない場合だと覚えることも多く、難しいところもあるかと思いますので、Swift Concurrencyを利用する方が理解しやすいかもしれません。ただし、ViewとPresenterの値のバインディング処理にも利用していますので、FAANS iOSでは当面、Combineも利用していくと思われます。
今回、async/awaitで書き換えた理由として、主に2つの理由があります。
- 非同期処理をシンプルに書けるようになるため
Combineのコードは、コールバックで受け取る必要があり、コールバックの中でさらに別のAPIを叩く場面もあります。async/awaitで手続型のように書けるので、シンプルな記述が可能です。本記事で実際のコード例を元に説明します。
- Swiftのアップデートに追従しつつ、チームとして継続的に新しい技術に触れることで成長していきたいため
URLSession等、Apple標準のAPIでasync/awaitがすでに使われており、今後も様々な機能がアップデートされます。キャッチアップした内容を業務で積極的に活用できる環境づくりをチームで心がけています。
本記事ではCombineでの非同期通信の処理に対しasync/awaitで書き換えたユースケースを紹介し、実装のポイント等、説明します。
FAANS iOSの構成
Combineを使用している箇所を中心に図示しました。Combineは非同期処理の他に、View/PresenterのBindingで利用しております。
async/await概要
本記事で登場するasync/awaitのキーワードは以下の通りです。
- async
- await
- async let
- withCheckedThrowingContinuation
- Task
- Task Group
- AsyncSequence
Swift Concurrencyでは新たな概念が色々出てきます。学習する際、何のキーワードについての説明かをマッピングしていくと理解しやすいです。Swift Concurrency チートシートで、キーワード毎に整理されていますので、とても参考になります。
async/awaitの基本として、asyncキーワードの理解が大事ですので説明します。一般的にコールバック関数をasync/awaitで書き換えると次のようなコードになります。
コールバックを返すコード
func downloadData(from url: URL, completion: @escaping (Data) -> Void)
downloadData(from: url) { data in
// コールバックでdata を使う処理
}
async/await書き換え後のコード
func downloadData(from url: URL) async -> Data
// コールバックで受け取ることなく、data変数に結果が格納され、手続型のように後続処理をかける
let data = await downloadData(from: url)
API Clientをasync/awaitで書き換え
FAANS iOSではAPIを実行するクラス、API ClientでCombineを利用しており、AnyPublisher型を返す関数があります。これをSubscribeすることでコールバックが返ってきますので、withCheckedThrowingContinuationを利用し、コールバック関数をラップします。また、開発の効率性を高めるためにすでに実装済みのコードを再利用している箇所があり、Combineによるリクエストのインタフェースが存在している状況です。
// CombineのみのAPI通信ではこの関数を利用
func responsePublisher<Response>(for requestBuilder: RequestBuilder<Response>) -> AnyPublisher<Response, APIClientError> {
requestBuilder.executeWithIDToken()
.mapError { .init($0) }
.eraseToAnyPublisher()
}
// 上記のCombineのコードをwithCheckedThrowingContinuationでラップするだけで、async/awaitの書き換えが可能
func response<Response>(for requestBuilder: RequestBuilder<Response>) async throws -> Response {
let canceller = Canceller()
return try await withTaskCancellationHandler { // Cancel処理の詳細は省く。
// withCheckedThrowingContinuation使用箇所
return try await withCheckedThrowingContinuation { continuation in
if Task.isCancelled {
continuation.resume(throwing: CancellationError())
return
}
// responsePublisherの結果をSubscribeして、continuation.resumeに渡す。
// 確実に1回、continuation.resumeを実行する必要がある。
canceller.cancellable = responsePublisher(for: requestBuilder)
.handleEvents(receiveCancel: {
continuation.resume(throwing: CancellationError())
})
.sink { completion in
switch completion {
case .failure(let error):
// エラーの場合はthrowingの方を利用
continuation.resume(throwing: error)
case .finished:
break
}
} receiveValue: { value in
// returningの方を利用し、結果を渡す
continuation.resume(returning: value)
}
}
} onCancel: {
canceller.cancel()
}
}
// 利用例
// Combineのまま
self.apiClient.responsePublisher(for: MemberAPI.getMember())
.sink { [weak self] (completion) in
switch completion {
case .failure(let error):
// エラー処理
case .finished:
break
}
} receiveValue: {[weak self] member in
// 結果をコールバックで受け取る
}
.store(in: &cancellables)
self.apiClient.responsePublisher(for: MemberAPI.getMember())
// async/await書き換え後
do {
// 結果がmemberに格納され、エラーの場合はcatchの方にいく
// Combineではコールバックで結果を受け取る形になる
let member = try await APIClient().response(for: MemberAPI.getMember())
catch {
// エラー処理
}
responsePublisher関数は、引数のリクエストを実行し、AnyPublisher型を返します。これをwithCheckedThrowingContinuationのクロージャ内で利用し、subscribeした結果をcontinuation.resumeに渡します。
withCheckedThrowingContinuationではエラーを扱うので、エラー時の処理も忘れないようにしましょう。また、Cancel処理を行うためにwithTaskCancellationHandlerを利用していますが、詳細は本記事では省きます。
続きはこちら