ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftのasync/awaitについて
Swiftは非同期関数をサポートするようになりました - 一般にasync/awaitとして知られているパターンです。新しい構文でどのようにコードが読みやすく、理解しやすくなるかをご確認ください。関数がサスペンドする際に起きる事柄を学び、既存のコンプリーションハンドラを非同期関数に適応させる方法を説明します。
リソース
- SE-0296: Async/await
- SE-0297: Concurrency Interoperability with Objective-C
- SE-0300: Continuations for interfacing async tasks with synchronous code
- SE-0310: Effectful read-only properties
- The Swift Programming Language: Concurrency
関連ビデオ
WWDC22
WWDC21
-
ダウンロード
♪ ♪ Nateです Swiftチームの エンジニアをしています 本日 Robertとともに お伝えするのはSwiftの async/awaitについてです 非同期のプログラミングは 皆さんの多くが 日常的に 行っていることでしょう そのため書いてしまいがちな 冗長で複雑な非同期コード 間違うことさえあります それに役立つのはSwiftの async/awaitです async/awaitを使えば 通常のコードと同様に 非同期コードを 簡単に書くことができます それにより アイデアは さらにコードに反映され 安全性も高まります さらにSDKには 待機可能なメソッドが数多くあります 例えばUIKitにあるのは UIImageからのサムネイル作成機能です 実際そのタスクを行う同期と非同期 両方の関数があります
簡単にご紹介しましょう 同期型の関数 つまり通常の古い関数を 呼び出すと スレッドはブロックされ その終了を待たなければなりません つまり fetchThumbnail関数は UIKitの同期型関数である preparingThumbnailを呼び出すため スレッドでは終了まで 何も実行できなくなるのです その一方 prepareThumbnail (of:completionHandler:)の非同期版を 関数の実行中に呼び出しても スレッドでは他の作業を実行できます 作業が完了したら完了ハンドラを 呼び出して通知します SDKには多くの非同期機能があり いくつかの方法で完了を 知らせてくれます このように 完了ハンドラを 使う場合もあれば デリゲートをコールバック する場合もあります 多くは 非同期と表示され 値が返されるだけです これらの非同期関数に 共通しているのは 関数を呼び出すと 作業を開始したスレッドの ブロック解除が 即座に行われるという点です 長時間に及ぶ作業を行っている間に そのスレッドで他の作業を 完了させることができます 違いを確認するため多くの方が よく目にする例を見てみましょう Robertと私が構築中のAppにある アイテムリストの各行には サーバーに保管されている 画像をサムネイルとして 表示しています サムネイルをリストに表示する 準備ができたらビューモデルでは fetchThumbnailメソッドが呼び出され 一連の手順で 文字列はUIImageに変換されます まず ビューモデルの thumbnailURLRequest メソッドが 文字列からURLRequestを作成します 次に URLSessionの dataTaskメソッドがその リクエストデータを取得します UIImage initWithDataにより データから画像が作成され 最後に UIImage prepareThumbnailメソッドで 元の画像のサムネイルが表示されます 各操作は それぞれ前の 操作結果により決定されます つまり 正しい順序で 行わなければなりません
操作の中には 文字列からのURLRequest作成 データからのUIImage作成など 素早く値を返すものもあり 関数を実行中の スレッドでも問題はありません これらは 同期的呼び出しとなりますが 時間がかかるものもあります 画像を構成するデータの ダウンロードがそうです また 見栄えのする サムネイルに仕上げるには 作業に時間を かけなければなりません そのため SDKには これらのタスクを行うための 非同期関数があります そのため この呼び出しは 非同期で行う必要があります Robertと私が async/awaitを試みる前は 完了ハンドラを使用して 関数を書いていました
この関数では引数としての文字列 最初の操作への入力と 呼び出し元に返すための 完了ハンドラを受け取ります fetchThumbnailが呼び出された場合 thumbnailURLRequestを 最初に呼び出します このメソッドは同期型なので 完了ハンドラは 必要ありません 次に dataTaskを 共有されたURLSession インスタンスで呼び出し URLRequestと 完了ハンドラを渡します URLSessionDataTaskが 同期的に生成されると 処理が再開され 非同期の作業が開始されます fetchThumbnailが戻ると スレッドでは 他の作業を 行うことができます 大きな問題となるのは 画像のダウンロード中に データのストリーミングを 待機しているスレッドを ブロックしたくないということです 最終的に行きつくのは 画像のダウンロード終了か 問題発生のどちらかです いずれにしてもリクエストは完了し dataTaskに渡された 完了ハンドラがオプションの 値とともに呼び出されます それらはデータ レスポンス エラー などです 問題が発生した場合は 完了ハンドラを呼び出し エラーを通知する必要があります 問題がなかった場合UIImageの initWithDataを使用して データから画像を作成します これらは同期型なので 結果を処理するためには 一般的な直線的コードを書きます 画像が生成されない場合は終了です 画像が生成された場合は最後に UIKitのprepareThumbnailメソッドを 呼び出し 完了ハンドラを渡します 作業を仕上げる間 スレッドのブロックは解除され 他の作業が可能になります
サムネイルが生成された後 サムネイルの準備に成功した場合 完了ハンドラは 画像とともに呼び出されるか nilとなります 成功した場合 完了ハンドラを呼び出し 画像を渡します
ですが Robertが指摘したように 問題があります fetchThumbnailの呼び出し元は 作業終了の通知を要求します 失敗した場合も同様です 現在は 呼び出し元を 放置している状態です 私は“guard else return”と 書くことに慣れ過ぎてしまい 完了ハンドラの呼び出しを 2度も忘れた事があります つまりデータからのUIImage生成や サムネイル作成の失敗などは fetchThumbnailの 呼び出し元に通知されず そのため 行がアップデートされる ことはありません スピナーが表示され続けるだけです
そのため fetchThumbnailの 制作者として とても重要なのは どんな状況でも呼び出し元に 通知するということです つまり 関数を介した すべてのパスによって 通知される必要があるのです それには エラー発生時に 完了ハンドラを呼び出し そのエラーを渡さなければなりません 通常の関数の場合 通知することで呼び出し元に エラーを返します Swiftの場合関数を介した実行の 進み具合にかかわらず 値が返されない場合は 確実にエラーが通知されます しかし Swiftでの通常の エラー処理方法は使えません 問題が発生した場合 これらの 完了ハンドラの中からエラーを 通知することはできません 残念ですが Swiftに作業を 確認させることはできません Swiftにとって fetchThumbnailのような 完了ハンドラは クロージャにすぎません 常に起動していることを 確認したくとも Swiftには起動を 強制する手段がありません だからこそ 例の2つのガードから戻った際 コンパイルエラーは出ませんでした Robertが指摘してくれたおかげで 解決することができました そのため 完了ハンドラが 呼び出されるかどうかは 最終的にあなた次第です 私たち2人がこの関数を書いた時には いくつかの操作を次々に 行いたいと思っていました 2つは同期型 2つは非同期型で 完了ハンドラを使用します 成功はしましたが 最終的に 約20行のコードには わずかなバグの可能性が 5ヶ所にありました 私たちが求めたものは 4つの操作を順番に行うことでしたが 成果としては順に実行することも 正しく実行することも難しく 意図は曖昧な状態です
しかし 安全性を 少し高める方法はあります 例えば 標準ライブラリのresult型を 使うこともできました これで安全性は高まりますが 形式を追加することで コードは見苦しくやや長くなります 先物のような技術を使い 非同期コードを 別の方法で改善した人もいます しかしこれらの方法では シンプルで簡単 安全な コードは書けません async/awaitを使えば さらに改善できます 私たちは関数を書き直しました 4つの手順を実行するものに そして 今回は async/awaitを使用しました
この関数も同様に 文字列を 引数として受け取ります 先ほどは 完了ハンドラも 渡していましたが 今回は代わりに 関数が非同期となっています 関数を非同期にする場合 関数シグネチャである "throws"の直前に キーワードを入れます 関数が通知されない場合は 矢印の前に入れます 関数を非同期にすることで 関数とそのシグネチャを よりシンプルにできます 画像のサムネイル作成に成功した場合 サムネイルはそのまま返されます エラーが発生した場合は そのまま通知されます fetchThumbnailが 呼び出されると 引きつづき thumbnailURLRequestが 呼び出されます この関数は同期型なので スレッドは ブロック状態で 作業をしています
次に 共有のURLSession上で data(for: request)を 呼び出し データをダウンロードします dataTask 同様 Foundation による メソッドのためこちらも非同期です しかし dataTaskとは異なり dataメソッドは待機可能です そのため 呼び出した後は 一時停止し スレッドの ブロックを解除します すると スレッドで 他の作業が可能となります
データメソッドの"throws"が "try"を導いています 以前のバージョンでは エラーを確認してから 完了ハンドラを明示的に 呼び出す必要がありました 待機可能版では これらのコードはすべて “try”キーワードだけに 集約されています "try"で呼び出せる関数が"throws"と 書かれているのと同様に "await"で呼び出される関数には "async"と書かれています 式の中に複数 非同期関数の 呼び出しがある場合 "await"の書き込みは 一度だけで問題ありません それは 関数の呼び出しを 複数行っている式でも "try"の書き込みが 一度で済むのと同様です つまり 関数呼び出しには "try await"と書かれます 通知する非同期の式を扱う場合 “await”の前に“try”を 置く必要があります
最終的に データのダウンロードが終了すると dataメソッドは再開され fetchThumbnailに戻ります その時 データメソッドが返す値 または 通知されるエラーの値です エラーが通知された場合 fetchThumbnail自身にも 同様にエラーが通知されます その他の場合 データと 応答変数は定義されます これは以前のバージョンで 起動されたURLSessionの dataTaskメソッドに完了ハンドラが 渡された時の fetchThumbnailと同様です
どちらのバージョンでも URLSessionの 非同期メソッドで生成された 値やエラーは現れました しかし 待機可能版は さらにシンプルです 私たちの言いたいことを そのまま伝えてくれます リクエストを実行し戻ってきた値を 変数に代入することで使用可能となり 問題が発生した場合には エラーを通知します
次に fetchThumbnailで ダウンロードしたデータから UIImageを作成してみます 成功した場合 プロパティに アクセスすることで その画像のサムネイルが表示されます サムネイル生成の間 そのプロパティが最終的に 再開して fetchThumbnailに 戻るまで スレッドでは 他の作業が可能です
サムネイルが表示されると fetchThumbnailで返されるか エラーが通知されます 完了ハンドラ版とは対照的に サムネイルが表示されない時 Swiftでは 確実に エラーを通知するか 値を戻しますが 黙って失敗もできません 以上です これが 必要なコードの全貌です 関数については 以前の完了ハンドラ版と まったく同様です しかし コードは20行でなく たったの6行です また 全て直線的コードです 正しい順序で実行すべき4つの操作が 次々とリストアップされます また Swiftでは問題が発生した場合に それを 戻すか 通知することで 関数の終了を 確実に 呼び出し元に通知します これは一例であり async/awaitの使用により 非同期のSwiftコードを より安全に より短く より意図を反映したものに 変換できるのです それではfetchThumbnailの 実行方法について詳しくご紹介します 最後から2行目に関数の呼び出しは ありませんが サムネイルの表示を 始める式には“await”と書かれています サムネイルのプロパティが 非同期のためです 関数以外も非同期にできます プロパティもイニシャライザと 同様に可能です サムネイルのプロパティは 現在 SDKには含まれません 実は Robertが追加してくれました 見てみましょう
このプロパティはUIImageの 拡張機能で定義され その実装は短整数です CGSizeを生成し byPreparingThumbnail(ofSize) に 渡した結果を待ちます ちなみにselfでのこのメソッドは 先ほど使用した メソッドの待機可能版です
いくつか注意すべき点もあります 第一に明示的なゲッターについてです これは プロパティを 非同期にするために必要です Swift 5.5ではプロパティの ゲッターも通知されます 非同期関数のシグネチャと同様に プロパティが asyncかつthrowsである場合 “async”キーワードは “throws”の直前に入ります
次に プロパティに セッターがない場合 読み取り専用プロパティのみ 非同期にできます
関数プロパティ イニシャライザにおいて awaitを式で使用することで 関数がブロック解除する 可能性があるスレッドを 示すことができます awaitには別の使用法もあります 非同期シーケンスを 反復するループ処理などです 非同期シーケンスは エレメントの送信が 非同期であることを除けば 通常のシーケンスと同じです そのため 次のアイテムを 取り出す際には awaitを付けて 非同期だと 示す必要があります
関数が非同期シーケンスを 何度もくり返すことで 次のエレメントを待つ間に スレッドのブロックが 解除されます その後 次のエレメントで ループ本体に移動するか エレメントがない場合は ループ処理の後に再開されます
AsyncSequenceについての詳細は 「AsyncSequenceについて」を をご覧ください また多くの非同期タスクを 同時に実行したい方は 「Swiftにおける構造化並行処理」 をご覧ください
awaitを使える場面は たくさんあります このキーワードには 非同期関数が停止される 可能性があります 非同期関数が停止されるとは? それに答えるには 関数を 呼び出した時のことを 考えてみましょう 任意の関数を呼び出した場合 関数を実行している スレッドのコントロールを 呼び出した関数に渡します 通常の関数 thumbnailURLRequestなどを 呼び出す場合 スレッドはその関数に 代わって作業を実行し 関数が終了するまでは 他の作業を行えません
その作業は関数本体 または その関数が呼び出す 他の関数で行われます 最終的にその関数が終了する時には 値が返されるかエラーが通知されます そうして コントロールが 関数に戻されます この唯一の方法により 通常の関数はスレッドの コントロールを渡し終了となります それは 関数によってのみ コントロールが可能です 非同期関数の呼び出しの場合 状況は異なります 通常の関数と同様に完了すると 終了して 関数に コントロールが戻されます しかし 通常の関数とは 全く異なる方法でスレッドの コントロールを渡します つまりサスペンドです
通常の関数と同様に スレッドのコントロールは 呼び出した非同期関数に渡されます
実行が始まると 同期関数は 停止可能になります そうなると スレッドの コントロールは渡されます しかし スレッドのコントロールは 関数ではなくシステムに戻されます そうなると 関数も停止してしまいます サスペンドとは 関数を システムに伝える手段です やるべき作業がたくさんあるので 最重要事項を決定してください” とても協力的です そのため 関数が停止されると システムは スレッド上で 他の作業を実行できます ある時点で先に中断した 非同期関数を続けて実行することが 最重要であるとシステムは判断し その時点でシステムは再開します すると 非同期関数は スレッドのコントロール下に 戻り 作業の続行が可能になります 必要であれば再度の停止も可能です 実際 停止は何度でも可能です 逆に 停止の必要が 全くない場合もあります 非同期関数に停止の 可能性はありますが asyncと書かれていても 停止しない場合もあります 同様に “await”と書かれていても 確実に 関数が停止される わけではありません しかし最終的には停止せずに または最後に再開した後でも その関数は終了し値やエラーとともに スレッドのコントロールを 関数に戻します
fetchThumbnailの サスペンド時に起きる事を もう一度確認してみましょう
fetchThumbnailが呼び出す URLSessionの 非同期データメソッドのみが 可能な方法で スレッドの 実行が停止されること これがサスペンドであり スレッドのコントロールを システムに委ね URLSessionのdataメソッドの作業を システムに組み込みます しかしこの時点でシステムは コントロールされており すぐに作業が始められない 可能性があります 代わりに スレッドを他の 作業にあてることができます 実際に見てみましょう fetchThumbnailが呼び出された後 データのアップロードのため ユーザーがボタンを タップしたと想定します 例えば あるポストに 反応したとしましょう するとシステムは 先に キューイングされた作業の 前に ユーザーの反応を 書き込む作業が可能になります
直前の作業が完了すると URLSessionのdataメソッドを 再開することができます または システムが別の作業を 実行することもあります 最後にdataメソッドが終了すると 再び fetchThumbnailに戻ります 関数の停止時に 他の作業を実行するため Swiftは awaitを使用した 非同期での呼び出しの 書き込みを求めます 関数の停止時はAppの状態が大きく 変わる可能性があるので ご注意ください
ちなみに こちらは 完了ハンドラを使用する 場合にも当てはまります しかしasync/awaitコードに伴う 形式やインデンテーションは ないため コードブロックが トランザクションとして 実行されないことは キーワードの awaitによって 通知されます 関数が停止したり関数の行間での 停止時に 別の事態が 起こる可能性もあります それだけでなく全く別のスレッドで 関数が再開されることもあります 「Swiftアクターによる ミュータブルステートの保護」 をご覧ください ここでasync/awaitの 重要事項を紹介します まず 関数にasyncと 書くことで その関数は 停止が可能になります 関数自体による停止は呼び出し元も 停止させます つまり 呼び出し元も 非同期である必要があります 次に 非同期関数の中で 停止する可能性がある 箇所を指摘するために “await”が使われます 第3に 非同期関数の 停止時 スレッドは ブロックされていません システムは他の作業を 組み込むことが可能です 後から開始された作業でも 先に実行することができます つまり 関数の停止時に Appの状態が大きく 変化する可能性があります 最後に非同期関数が再開すると 呼び出された非同期関数が返す結果は 元の関数に戻され 中断したところから 実行は再開されます Swiftでのasync/awaitの 仕組みを見てきました 次に Robertから プロジェクトでの活用方法を 紹介してもらいます ありがとう Nate 先ほど 私たちが構築中の Appをご紹介しました async/await採用のために 変換したthumbnail関数は 複数の場面で呼び出されたため 並行処理を行うためにも それらの移行が必要です 現代のソフトウェア開発における 必須事項から始めましょう それはテストです 非同期コードのテストを 同期コードと同様に行うため 革新的な XCTestによる非同期の サポートが行われます 以前は期待値を設定し テスト対象のAPIを呼び出し 期待値を満たし任意の時間待つという 面倒なプロセスでしたが 今後は テストを行う関数に “async”を追加しXCTestの期待値や その実行結果明示的なawaitを削除し 先ほどNateから紹介があった 非同期のfetchThumbnail関数の 呼び出し結果を待つことになります
テストについては終わり Appコードを見てみましょう 注目は リスト各行のサムネイル表示の 背後にあるSwiftUIコードです
画像セルはポストごとに作成され 各ポストのIDを viewModelに渡すことで 非同期でのサムネイル取得が 可能となります テストコードから呼び出しを 変換する方法は すでにご覧いただきました では 試してみましょう まず完了ハンドラを削除し エラーを処理する“try”と 非同期関数の呼び出しを 完了する“await”を追加します しかし このコードは うまく作成できません Swiftのコンパイラによると それ自体が非同期ではないため 非同期関数を 呼び出せないということです ここではonAppearの修飾子が 非同期かつプレーンな クロージャを取っているため 同期と非同期のギャップを 埋める方法が必要になります
解決するには 非同期の タスク関数を使用します 非同期タスクとは クロージャ内の作業を まとめてシステムに送り 次に利用可能なスレッドでの 実行を準備するもので グローバルディスパッチ キューの非同期関数と同様 ここでの主な利点は 同期コンテキストの中から 非同期コードを 呼び出すことができる点です
作成し直すことで コンパイラは成立します 非同期タスクは API系統に不可欠な要素で 使いやすく 自然に 構造化されたスタイルで 内容豊富な並列の Swiftコード作成が可能です 「Swiftにおける構造化並行処理」 を ご覧ください また SwiftUI Appでの 非同期コード活用法は 「SwiftUIでの並行処理」 をご覧ください
fetchThumbnail関数を呼び出していた 場所の移行はすべて終了しています しかし Appにasync/awaitを 採用する機会は多くあります すぐに運用を開始するために 既存のAPIへの 非同期の代替手段導入から 始めることをお勧めします SDKには完了ハンドラを使用する 多くのAPIがあり 我々に代わって 非同期的に作業を行います これらのAPIを並べてみると パターンが見えてきます
これらの機能は名称や目的が 異なっていてもAPIコントラクトは 本質的には同じです それらを呼び出すと 提供された完了ハンドラを 使用して呼び戻され 得られた結果が渡されます 先ほど Nateから紹介があったように 非同期関数の結果を待つことで より自然なコードを 書くことができます コールバックブロックを非同期関数に 変換できたら 素晴らしいですよね Swift 5.5では まさに これが実現しました SwiftのコンパイラはObjective-Cから インポートされた 完了ハンドラのコードを 自動的に確認し 非同期の 代替手段を提供します それだけではありません デリゲート型APIにも 完了ハンドラを渡す メソッドが多くあります ハンドラを呼び出すと非同期タスクが 完了した時点で フレームワークに 協調的に通知されます 例えば ClockKitの 複雑なデータソースでは fetchThumbnailを呼び出して 規定のポストのタイムライン エントリを表示します
先ほど同様 すべてのパスで 完了ハンドラを 呼び出す必要がありますが クロージャが原因となり 不必要なノイズも多く存在しています
async/awaitではそうはなりません このデリゲートメソッドには 代わりに使用できる 非同期の代替手段があります まず 非同期の代替手段の 名前として使用するため 先頭の“get”を削除します 非同期関数では呼び出しの結果が 直接返されないことを 知らせる“get”などの 先頭の語を 削除することをお勧めします 結局のところ非同期の代替関数のため タイムラインエントリは 直接返されます 非同期コンテキストを設定したので 非同期版のfetchThumbnailを 呼び出します 最後に タイムラインエントリを メソッドから返し 削除済の完了ブロックの 呼び出しは行いません
ご紹介した非同期型APIは ほんの一部に過ぎません APIそのものやasync/awaitを 採用する際の使い方についての詳細は こちらのセッションをご覧ください
これらすべては Swiftがあなたに代わって 非同期の代替手段を 作成する状況の例です しかし コードの中には 非同期の代替手段を 自分で作らなければならない 場所が必ずあります
どのようなものか 実際に見てみましょう
このAppでは getPersistentPosts関数を 使い Core Dataの記憶装置に 永続化した ポストを取得します この関数は App内での 呼び出しが非同期の サムネイル関数よりも多く すべての場所で 非同期を使用するという点で 大きな変更です また 我々が使用している NSAsynchronousFetchRequest という関数は 非同期の代替手段として 完璧な候補と言えそうです まず 非同期関数を作り 戻り値を変換します この関数にはエラー発生の 可能性があるので この関数にも“throws”と書きます
次に 完了ハンドラ版の getPersistentPostsを 呼び出しますが これではうまくいきません
コールバックの結果を 非同期のpersistentPosts関数の 呼び出しを待っている場所に 戻す必要があります それだけでなく 呼び出し元は停止状態です 私たちは適切なタイミングと 適切なデータで復帰させ 他の作業を 実行可能にしなければなりません
先ほど Swiftとシステムが共同で行う 非同期コードの復帰処理の 様子を紹介しました サスペンドとリジュームの プロセスがどのように 機能するかを知ることで 問題解決のヒントがあるか もう少し見てみましょう
非同期版のpersistentPostsが 呼び出されCore Dataを呼び出します その後 Core Dataは 完了ハンドラを呼び出し フェッチリクエストの結果を渡します こちらは 先ほど紹介した fetchThumbnail関数が 停止した非同期関数呼び出しの再開を Core Dataではなく システムに要求した時の 状況とほぼ同じです
あとは 完了ハンドラを待ち フェッチリクエストの 結果を伴って再開するための ブリッジが必要です このパターンはよく現れるもので その名前を“継続”といいます 今回のセッションではNateと私から 継続についての 多くの例をご紹介しました それは 完了ブロックを とるメソッドでした
メソッドの呼び出し元は 関数呼び出しの結果を待ち 次に何をすべきかを指定する クロージャを提供します 関数の呼び出しが完了すると 完了ハンドラが呼び出され 結果に対して 呼び出し元が要求する すべての作業が再開されます このような協調実行は Swiftにおいて 非同期が機能する仕組みと まったく同じです これを明確にするために Swiftには ハイレベルかつ 安全な方法で 継続処理を作成 管理 再開 できる機能があります
先ほどの例に戻り 継続処理がどのようにして 非同期の代替手段を書くのに 役立つかを見てみましょう
withCheckedThrowing- Continuation関数は エラーが発生した 完了ブロックを Swiftの 非同期型スローイング関数 までリフトします withCheckedContinuationsは 関数が絶対にエラーを出さないと わかっている状況に対応するものです これらの関数は 停止された非同期関数を 再開するために使用可能な 継続の値に アクセスするための手段です これでgetPersistentPostsの 呼び出し待機が 可能となり ブリッジの 最初の部分も構築されます
それでは ブリッジを完成させましょう 継続の値により 完了ハンドラからの結果を 置くためのレジューム関数が 提供されます 加えて レジュームは persistentPosts関数の 結果の呼び出し停止を 解除するために必要な ミッシングリンクを提供します これで 適切なパッケージに 完了ハンドラから 非同期関数への完成した ブリッジが作られます
継続処理とは 強力な方法であり 非同期関数の実行を 手動でコントロールするものですが 注意すべき点が いくつかあります 継続にはシンプルかつ重要な コントラクトがあること レジュームは 各パスで 確実に1回呼び出すこと ですが 心配は無用です Swiftは あなたの味方です
レジュームが呼び出されずに 継続が破棄された場合 Swiftのランタイムは 警告を記録し 非同期の呼び出しが 停止されることはありません しかし 同一関数内で複数回 継続が再開された場合は より深刻なエラーとなり プログラムデータが 破損する可能性があります この問題に対処するため Swiftのランタイムは レジュームの複数回呼び出し を検出し 2回目の 再開点で決定的なエラーが 発生するようにします
これを念頭に置いて チェックされた継続を 使用する可能性のある別の 重要な場所を紹介しましょう
APIの多くはイベント駆動型です デリゲートコールバックを することで これらは 特定の重要なポイントでAppに通知し Appが適切に対応できるようにします async/awaitを 適切に採用するためには 継続を保存して 後で再開する必要があります 先ほど同様 チェック された継続を作成します
その後 それを保存し 作業を開始します
チェックされた継続の APIコントラクトを優先するために 作動中の継続を確実に再開し 最後にnilにすることで 2回以上の呼び出しが 起きないようにします
常に覚えておいてほしいのは チェックされた継続の値が ここで表すのは APIへの 非同期呼び出しに対する 手動での再開機能であり それは すべてのパスで 呼び出される必要があります デリゲート型APIが 何度も呼び出される場合や 特定の状況で全く呼び出されない場合 作動中の継続を 確実に1回 だけ再開することが重要です
継続を含む Swiftの 並行処理における 下位レベルの詳細については 「SSwiftの並行処理: 舞台裏」 をご覧ください
Swiftでのasync/awaitを 駆け足でご紹介しました “async”と“await”が 実行時に機能する仕組み また Appやフレームワークに 採用する方法を紹介してきました 運用開始に際しSDKにおいて 利用可能な非同期APIの 一部をご紹介し 既存のコードを 同期から 非同期の世界に移行する 方法をご紹介しました
async/awaitとは Swiftの並行処理機能 全体の基礎となるものです それを活用して 皆さんが 作り上げるものが楽しみです ありがとうございました
-
-
3:43 - Writing a function using completion handlers
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() }
-
8:00 - Using completion handlers with the Result type
func fetchThumbnail(for id: String, completion: @escaping (Result<UIImage, Error>) -> Void) { let request = thumbnailURLRequest(for: id) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(error)) } else if (response as? HTTPURLResponse)?.statusCode != 200 { completion(.failure(FetchError.badID)) } else { guard let image = UIImage(data: data!) else { completion(.failure(FetchError.badImage)) return } image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in guard let thumbnail = thumbnail else { completion(.failure(FetchError.badImage)) return } completion(.success(thumbnail)) } } } task.resume() }
-
8:30 - Using async/await
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 }
-
13:15 - Async properties
extension UIImage { var thumbnail: UIImage? { get async { let size = CGSize(width: 40, height: 40) return await self.byPreparingThumbnail(ofSize: size) } } }
-
14:17 - Async sequences
for await id in staticImageIDsURL.lines { let thumbnail = await fetchThumbnail(for: id) collage.add(thumbnail) } let result = await collage.draw()
-
21:22 - Testing using XCTestExpectation
class MockViewModelSpec: XCTestCase { 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) } }
-
21:56 - Testing using async/await
class MockViewModelSpec: XCTestCase { func testFetchThumbnails() async throws { XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID)) } }
-
22:30 - Bridging from sync to async
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) } } } }
-
25:56 - Async APIs in the SDK
import 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 } let entry = self.createTimelineEntry(for: thumbnail, date: date) return entry } }
-
26:59 - Async alternatives and continuations
// Existing function func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) { do { let req = Post.fetchRequest() req.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in completion(result.finalResult ?? [], nil) } try self.managedObjectContext.execute(asyncRequest) } catch { completion([], error) } } // Async alternative func persistentPosts() async throws -> [Post] { typealias PostContinuation = CheckedContinuation<[Post], Error> return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in self.getPersistentPosts { posts, error in if let error = error { continuation.resume(throwing: error) } else { continuation.resume(returning: posts) } } } }
-
31:44 - Storing the continuation for delegate callbacks
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 // guard against multiple calls to resume } func peerManager(_ manager: PeerManager, hadError error: Error) { self.activeContinuation?.resume(throwing: error) self.activeContinuation = nil // guard against multiple calls to resume } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。