【Swift Concurrency】withCheckedContinuationとwithCheckedThrowingContinuationの使い方
この記事からわかること
- Swift Concurrencyとは?
- withCheckedContinuationとwithCheckedThrowingContinuationの使い方
- 非同期処理の実装方法
- asyncとの違い
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:15.0.1
- iOS:17.1
- Swift:5.9
- macOS:Sonoma 14.1
公式リファレンス:Concurrency
公式ドキュメント:Concurrency
Swift Concurrencyとは?
Swift Concurrency(同時実効性)とはiOS15(Swift 5.5)から導入された仕組みの1つで非同期プログラミング(並列処理)をより利用しやすくするための機能を提供しています。async
やawait
キーワード、Task
構造体などはSwift Concurrencyから提供されており、非同期処理を実装する際に利用されるcompletionHandler(コールバック関数)の弱みである可読性の低下を解消することができるようになりました。
今回はその中のwithCheckedContinuation
とwithCheckedThrowingContinuation
についてまとめていきます。
withCheckedContinuationとwithCheckedThrowingContinuationの使い所
先にwithCheckedContinuation
とwithCheckedThrowingContinuation
の使い所と役割を整理しておきます。この2つのメソッドはSwift Concurrencyから提供されている非同期処理を実装するためのメソッドです。といっても非同期処理の実装はasync/await
を使用すれば実装できます。
両者のメソッドの引数と見てみるとそのまま別の関数(クロージャー)を渡せるようになっています。
公式リファレンス:withCheckedContinuationメソッド
func withCheckedContinuation<T>(
function: String = #function,
_ body: (CheckedContinuation<T, Never>) -> Void
) async -> T
なので使い所としては既存のコードを非同期関数に簡単に改修したい場合に活用できます。Swift Concurrencyを導入せずにcompletionHandler(コールバック関数)などで非同期的な処理を実装していた場合に、コード量が多かったり、複雑だとasync/await
に対応するコストが大きくなってしまいます。これを時短できるのがwithCheckedContinuation
とwithCheckedThrowingContinuation
になるようです。
それぞれの違いは引数に渡す関数がエラーをthrowsするかしないかなので実装方法はほとんど同じです。
実装例
例えばまず既存のコードでにサーバーからデータを取得するfetchData
メソッドがあるとします。これはcompletionHandler(コールバック関数)で今までは実装されていました。
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
// 実際はAPIリクエストを送信
sleep(2)
if Bool.random() {
completion(.success(Data()))
} else {
completion(.failure(NSError(domain: "ErrorDomain", code: 42, userInfo: nil)))
}
}
これを非同期関数として改修したい場合に上記の場合は簡素なのでasync/await
でも簡単ですが、もっと複雑なコードになっていると大変なのでwithCheckedThrowingContinuation
を使用します。
func asyncFetchData() async throws -> Data {
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Data, Error>) in
fetchData { dataResult in
switch dataResult {
case .success(let data):
// 非同期処理が成功した場合
continuation.resume(returning: data)
case .failure(let error):
// 非同期処理が失敗した場合
continuation.resume(throwing: error)
}
}
}
}
Task {
let result = try await asyncFetchData()
print(result) // 0 bytes
}
このように元の関数をいじることなくラップするだけで非同期関数として実装することが可能になります。エラーを投げない場合はwithCheckedContinuation
を使用するだけです。
引数:CheckedContinuation<T, Never>
公式リファレンス:CheckedContinuation構造体
引数body
では(CheckedContinuation<T, Never>) -> Void
形式(クロージャー)になっています。クロージャーの引数として取得できるCheckedContinuation
型は同期的な元の関数を非同期関数として繋ぎ込むためのインターフェース的な役割を持っています。
resume
メソッドを使用して非同期関数としての結果を操作します。
// 非同期処理が成功した場合
continuation.resume(returning: data)
// 非同期処理が失敗した場合
continuation.resume(throwing: error)
おまけ:async/awaitに置き換える
ちなみに先ほどのfetchData
をasync/await
に置き換えると以下のような感じでしょうか?
func fetchData() async throws -> Data {
// 実際はAPIリクエストを送信
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2秒待機 (1秒 = 1_000_000_000ナノ秒)
if Bool.random() {
return Data()
} else {
throw NSError(domain: "ErrorDomain", code: 42, userInfo: nil)
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
参考文献:Studyplus iOSアプリにasync/awaitを導入してみた