Swift での非同期/待機
ハイライト
async/await を使用すると、同期コードと同じくらい直感的に非同期コードを作成できます。もともと 20 行のネストされたクロージャを必要としたサムネイル取得関数は、async/await を使用した 6 行の線形コードのみを必要とし、エラー処理は渡されます。
throws自然なコミュニケーション、あらゆるレベルで判断する必要がなくなりましたerror != nil。
主な内容
非同期プログラミングは日常的な開発で避けられないテーマですが、完了ハンドラーに基づくメソッドには共通の問題があります。非同期ステップが増えるほど、コードは指数関数的に膨らみます。ネットワークリクエスト、画像デコード、サムネイル生成という 3 段階の処理が入れ子になり、インデントは深くなり、エラー処理も各所に散らばります。少し注意を怠ると制御フローは発散します。
Swift 5.5 の async/await によって、この状況は根本から変わります。中心となる考え方はシンプルです。非同期関数には async を付け、await で現在の実行フローを一時停止して結果を待ちます。コンパイラがスレッド切り替えと状態保存を管理するため、コードは同期ロジックのように読めますが、実行はノンブロッキングです。
詳細
完了ハンドラーから async/await へ
サムネイルを取得する関数を例に挙げます。従来の完了ハンドラーの作成 (03:43):
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(nil, FetchError.badID)
} else {
guard let image = UIImage(data: data!) else {
completion(nil, FetchError.badImage)
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(nil, FetchError.badImage)
return
}
completion(thumbnail, nil)
}
}
}
task.resume()
}
このコードは 20 行あり、エラー処理は 3 か所に分散されており、各分岐は手動で呼び出す必要があります。completion。代わりに使用してくださいResult型 (08:00) によりエラー処理がより標準化されますが、コードの量は減っていません。
func fetchThumbnail(for id: String, completion: @escaping (Result<UIImage, Error>) -> Void) {
// ...同じネストされた構造ですが、completion(.failure(error)) が completed(nil, error) に置き換わります。
}
async/await バージョン (08:30) では、同じロジックが 6 行に凝縮されています。
func fetchThumbnail(for id: String) async throws -> UIImage {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
let maybeImage = UIImage(data: data)
guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage }
return thumbnail
}
主な変更点:
async関数を非同期としてマークし、throwsそしてasync自然な組み合わせawait現在の実行を一時停止し、スレッドは他の作業を行うために解放されます。- エラーが渡されました
throws上向きに伝播すると、各ブランチを手動で処理する必要がなくなります。 - コードの実行順序は、左から右、上から下の読み取り順です。
非同期プロパティ
UIキットprepareThumbnail同期と非同期の 2 つのバージョンがあります。できるUIImage非同期の計算プロパティを定義します (13:15):
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}
直接使用するawait image.thumbnail、通常のプロパティにアクセスするのと同じくらい自然です。
非同期シーケンス
バッチデータを処理する場合、async/await およびforループを結合して非同期シーケンスを形成します (14:17):
for await id in staticImageIDsURL.lines {
let thumbnail = await fetchThumbnail(for: id)
collage.add(thumbnail)
}
let result = await collage.draw()
staticImageIDsURL.linesですAsyncSequence、各反復await次の行を読んでください。ループ全体が順番に実行されますが、毎回awaitスレッドが解放されます。
非同期コードをテストする
テスト完了ハンドラー スタイル コードには次のものが必要ですXCTestExpectation(21:22):
func testFetchThumbnails() throws {
let expectation = XCTestExpectation(description: "mock thumbnails completion")
self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
XCTAssertNil(error)
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
async/await バージョン (21:56) は直接呼び出すことができます。
func testFetchThumbnails() async throws {
XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
}
同期コードからの非同期関数の呼び出し
SwiftUIonAppear同期コンテキストであり、作成する必要がありますTask非同期関数を呼び出すには (22:30):
struct ThumbnailView: View {
@ObservedObject var viewModel: ViewModel
var post: Post
@State private var image: UIImage?
var body: some View {
Image(uiImage: self.image ?? placeholder)
.onAppear {
Task {
self.image = try? await self.viewModel.fetchThumbnail(for: post.id)
}
}
}
}
既存の API のブリッジ: CheckedContinuation
非同期バージョンをまだ提供していないサードパーティ API の場合は、次を使用します。withCheckedThrowingContinuationパッケージ化 (26:59):
// 既存の完了ハンドラー API
func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) { ... }
// 非同期バージョンとしてラップされる
func persistentPosts() async throws -> [Post] {
return try await withCheckedThrowingContinuation { continuation in
self.getPersistentPosts { posts, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: posts)
}
}
}
}
デリゲート コールバック モードの場合、継続をインスタンス変数として保存します (31:44)。
class ViewController: UIViewController {
private var activeContinuation: CheckedContinuation<[Post], Error>?
func sharedPostsFromPeer() async throws -> [Post] {
try await withCheckedThrowingContinuation { continuation in
self.activeContinuation = continuation
self.peerManager.syncSharedPosts()
}
}
}
extension ViewController: PeerSyncDelegate {
func peerManager(_ manager: PeerManager, received posts: [Post]) {
self.activeContinuation?.resume(returning: posts)
self.activeContinuation = nil // 防止多次调用 resume
}
}
SDK の非同期 API
iOS 15 SDK は、多くの既存 API に async 版の代替手段を提供します (25:56)。たとえば ClockKit のコンプリケーションデータソースは次のようになります。
extension ComplicationController: CLKComplicationDataSource {
func currentTimelineEntry(for complication: CLKComplication) async -> CLKComplicationTimelineEntry? {
let date = Date()
let thumbnail = try? await self.viewModel.fetchThumbnail(for: post.id)
guard let thumbnail = thumbnail else { return nil }
return self.createTimelineEntry(for: thumbnail, date: date)
}
}
重要なポイント
-
「アセンブリ ロジック」の移行を優先する: まず、複数の非同期操作を呼び出す最上位関数を async/await に変更します。その後、基礎となるツール関数が徐々に従うことができます。こうすることで、変更の範囲は最小限に抑えられ、メリットは最大になります。
-
使用
withCheckedContinuationレガシー API のラッピング:まだ更新されていないサードパーティ ライブラリまたは内部 API この場合、それらをブリッジします。resume、途中で残りのポイントを「しないでください」に挿入します。 -
テスト コードに直接的な利点: async/await により、非同期テスト コードと同期テスト コードが同じ方法で記述され、不要になります。
XCTestExpectationそしてwait(for:timeout:)。 -
SDK の非同期代替バージョンに注意してください: iOS 15 以降、URLSession、Core Data、ClockKit などはすべて非同期 API を提供します。移行するときは、自分でパッケージ化するのではなく、ネイティブの非同期バージョンを使用することを優先してください。
-
Taskこれは同期から非同期への架け橋です:onAppear、IBAction他の同期コンテキストでは、次を使用します。Task { }非同期実行環境を作成します。Task.initほとんどの場合、現在のアクター コンテキストを継承することが正しい選択です。
関連セッション
- WWDC2021 10134 - Exploring Structured Concurrency in Swift — タスク、タスクグループ、および構造化同時実行の詳細な説明
- WWDC2021 10019 - SwiftUI の同時実行 — SwiftUI での async/await の特定のアプリケーション
- WWDC2021 10017 - Core Data Concurrency and Swift — Core Data が新しい同時実行モデルにどのように適応するか
- WWDC2021 10216 - Swift の ARC — オブジェクトのライフサイクルと同時実行の安全性
コメント
GitHub Issues · utterances