今携わっているプロダクトのコードは、コールバックだらけなので
コールバック周りをいじるような時は、Swift Concurrency(並行処理)のasync/awaitに書き換えをしています。
...が、その際にどうしてもコールバック関数からasync関数に変更できない場合がありますよね。
(影響範囲が広いとか、UIKitやFoundationのメソッドがまだコールバックしか対応していないとか)
そういった時に、どのようにAsync関数内でコールバック関数を呼び処理の完了を待つのか。
書き換え方法をまとめておきます。
結論
エラーを扱うか否かで、2通りの方法があります。
withCheckedContinuataion
または withCheckedThrowingContinuation
という関数を使います。
これは、Swift 標準ライブラリが提供する API です。
ErrorをThrowしないコールバックの場合
withCheckedContinuataion
を使用します。
// 元のコールバック関数
func fetchData(completion: @escaping (String) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
completion("データ取得完了")
}
}
// 書き換え後
func fetchDataAsync() async -> String {
return await withCheckedContinuation { continuation in
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
continuation.resume(returning: "データ取得完了")
}
}
}
Task {
print("データ種取得開始")
let result = await fetchDataAsync()
print(result)
}
// 出力結果:
// データ種取得開始
// (...2秒経過)
// データ取得完了
Errorを扱う必要がある場合
withCheckedThrowingContinuation
を使用します。
// 変換前の関数
func fetchData(completion: @escaping (String?, Error?) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
let success = Bool.random()
if success {
completion("データ取得完了", nil)
} else {
completion(nil, NSError(domain: "NetworkError", code: -1, userInfo: nil))
}
}
}
// 変換後の関数
func downloadData(from url: URL) async throws -> String {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
let success = Bool.random()
if success {
continuation.resume(returning: "データ取得完了")
} else {
continuation.resume(throwing: NSError(domain: "NetworkError", code: -1, userInfo: nil))
}
}
}
}
注意
ドキュメントにも記載がありますが、resume()
は必ず呼ぶようにしてください。
また、分岐がある場合には全てのケースにおいてresume()
を呼ぶようにしてください。
resume()
を忘れた場合、呼び出し元のawait側が永遠に待ち続けてしまいます。
また、resume()
を忘れた分岐で呼び出し元が待ち続けている間に、resume()
を実装した分岐に処理が通るとクラッシュします。