0Pricing
Swift Academy · レッスン

従来のコールバック/Combineとasync/awaitの橋渡し

withCheckedContinuation / withCheckedThrowingContinuation で完了ハンドラーをラップし、 Combine のpublisherを AsyncSequence に橋渡しして、シンプルな for await ループで扱います。

「従来のコールバック/Combineとasync/awaitの橋渡し」はCoddyKit上の無料Swift Academyレッスンです。 これはレッスン2/3です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはSwift Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Swift Academyコースには全3レッスンが含まれています。

ブリッジする理由

目標:continuationを使ってコールバックをラップし、CombineのストリームをAsyncSequenceとして扱います。

  • continuation(non-throwing/throwing)
  • URLSessionのブリッジ例
  • Combine → for awaitによる消費

非throwing継続

レガシーAPIが失敗しない場合はwithCheckedContinuationを使います。resumeは必ず1回だけ呼び出してください。

// Old-style API
func fetchName(completion: @escaping (String) -> Void) {
    // simulate async
    Task { try? await Task.sleep(nanoseconds: 80_000_000); completion("Ana") }
}

// Async wrapper using continuation (non-throwing)
func fetchName() async -> String {
    await withCheckedContinuation { (cont: CheckedContinuation<String, Never>) in
        fetchName { value in cont.resume(returning: value) }
    }
}

Task {
    let name = await fetchName()
    print("name:", name) // "Ana"
}

throwing継続

withCheckedThrowingContinuationを使うと、レガシーエラーをthrowで表面化できます。resume経路は1つだけにしてください。

enum NetError: Error { case offline; case badStatus }

func loadNumber(completion: @escaping (Result<Int, Error>) -> Void) {
    Task {
        try? await Task.sleep(nanoseconds: 80_000_000)
        completion(.failure(NetError.offline))
    }
}

func loadNumber() async throws -> Int {
    try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Int, Error>) in
        loadNumber { result in
            switch result {
            case .success(let v): cont.resume(returning: v)
            case .failure(let e): cont.resume(throwing: e)
            }
        }
    }
}

Task {
    do {
        let n = try await loadNumber()
        print(n)
    } catch {
        print("error:", error) // offline
    }
}

URLSessionのブリッジ

完了ハンドラーAPI(例:URLSession)をラップして、すっきりしたasync throws関数を作成します。resumeは必ず1回だけ呼び出すことを忘れないでください。

import Foundation

func getText(from url: URL) async throws -> String {
    try await withCheckedThrowingContinuation { (cont: CheckedContinuation<String, Error>) in
        let task = URLSession.shared.dataTask(with: url) { data, resp, err in
            if let err = err { cont.resume(throwing: err); return }
            guard let data = data, let text = String(data: data, encoding: .utf8) else {
                cont.resume(throwing: URLError(.badServerResponse)); return
            }
            cont.resume(returning: text)
        }
        task.resume()
    }
}

// Demo (works in environments with network access)
let demoURL = URL(string: "https://example.com")!
Task {
    do { let body = try await getText(from: demoURL); print(body.prefix(15)) }
    catch { print("network error:", error) }
}

CombineをAsyncSequenceとして使う

多くのCombine publisherは.valuesを公開しているため、カスタムブリッジなしで、発行された値をfor awaitで反復処理できます。

import Combine
import Foundation

let subject = PassthroughSubject<Int, Never>()

// Consume Combine as AsyncSequence:
Task {
    var produced: [Int] = []
    for await value in subject.values {          // Publisher.values -> AsyncSequence
        produced.append(value)
        if produced.count == 3 { break }         // stop after 3 values
    }
    print("collected:", produced)                // e.g., [1,2,3]
}

// Publish a few values
var cancellables = Set<AnyCancellable>()
Timer.publish(every: 0.05, on: .main, in: .common)
    .autoconnect()
    .prefix(3)
    .scan(0) { acc, _ in acc + 1 }
    .sink { subject.send($0) }
    .store(in: &cancellables)

Publisher → AsyncStream

.valuesを利用できない場合は、AsyncStreamを使って小さなブリッジを作成します。完了時に購読をキャンセルすることを忘れないでください。

import Combine

// Bridge ANY Publisher to AsyncSequence using AsyncStream (simple version)
extension Publisher {
    func asAsyncStream() -> AsyncStream<Output> {
        AsyncStream { continuation in
            let cancel = self.sink(
                receiveCompletion: { _ in continuation.finish() },
                receiveValue: { value in continuation.yield(value) }
            )
            continuation.onTermination = { _ in cancel.cancel() }
        }
    }
}

// Demo:
let pub = [10, 20, 30].publisher
Task {
    var sum = 0
    for await v in pub.asAsyncStream() { sum += v }
    print("sum:", sum) // 60
}

throwing continuationを使う

確認問題:失敗するコールバックをasyncにブリッジする最善の方法は何ですか?

まとめ

まとめ:レガシーコールバックをcontinuation(throwingまたはnon-throwing)でラップし、.valuesまたは小さなAsyncStreamブリッジを使ってCombineを消費します。resumeは1回だけにし、完了時には購読をキャンセルしてください。

よくある質問

「従来のコールバック/Combineとasync/awaitの橋渡し」レッスンは無料ですか?

はい。「従来のコールバック/Combineとasync/awaitの橋渡し」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Swift Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Swift Academyコースには全3レッスンが含まれています。

「従来のコールバック/Combineとasync/awaitの橋渡し」で何を学びますか?

withCheckedContinuation / withCheckedThrowingContinuation で完了ハンドラーをラップし、 Combine のpublisherを AsyncSequence に橋渡しして、シンプルな for await ループで扱います。 ブラウザで直接実行するハンズオンコードでSwift Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Swift Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのSwift Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/3です。

「従来のコールバック/Combineとasync/awaitの橋渡し」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このSwift Academyレッスンでコードを書いて実行できますか?

はい。すべてのSwift Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. プロデューサー/コンシューマー、パイプライン、タイムアウト
  2. 従来のコールバック/Combineとasync/awaitの橋渡し
  3. 非同期コードのテスト(XCTest)
← Swift Academyに戻る