1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Swift】コールバック関数を async / await に書き換える

Posted at

今携わっているプロダクトのコードは、コールバックだらけなので
コールバック周りをいじるような時は、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()を実装した分岐に処理が通るとクラッシュします。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?