
-
Code Along:Swiftの並行処理によるアプリの強化
Swiftの並行処理によって既存のサンプルアプリをアップデートして、アプリのユーザー体験を最適化する方法を学びます。まずメインアクターのアプリから始め、必要に応じて非同期コードを段階的に導入していきます。タスクを使用してメインアクター上で実行されるコードを最適化し、バックグラウンドにタスクをオフロードすることでコードを並列処理する方法を確認します。データ競合に対する安全性のメリットと、データ競合安全性に関するエラーの解釈と修正の方法について説明し、最後にアプリの観点で構造化された並行処理を最大限に活用する方法を紹介します。
関連する章
- 0:00 - イントロダクション
- 2:11 - アプローチしやすい並行処理の構成
- 2:51 - サンプルアプリのアーキテクチャ
- 3:42 - フォトライブラリからの写真の非同期読み込み
- 9:03 - 写真からのステッカーとカラーの抽出
- 12:30 - バックグラウンドスレッドでのタスク実行
- 15:58 - タスクの並列処理
- 18:44 - Swift 6によるデータ競合の回避
- 27:56 - 構造化された並行処理における非同期コードの制御
- 31:36 - まとめ
リソース
関連ビデオ
WWDC25
WWDC23
-
このビデオを検索
こんにちは SwiftとSwiftUI担当のSimaです この動画では Swiftの並行処理を使い アプリをレベルアップする方法を学びます アプリデベロッパが書くコードは ほとんどがメインスレッドにあります
シングルスレッドコードはわかりやすく メンテナンスも簡単です ただ 最新のアプリでは ネットワークリクエストや 負荷の高い演算など 時間のかかる タスクが必要なケースは珍しくありません このような場合は アプリの応答性を維持するため メインスレッドから作業を移動させます Swiftは 並行処理コードを書くのに 必要なすべてのツールを備えています このセッションでは 皆さんとアプリを 構築しながら その方法を説明します まず シングルスレッドアプリから始め 必要に応じて徐々に 非同期コードを導入していきます 次に 負荷の高いタスクの一部を オフロードして 並列処理することで アプリのパフォーマンスを向上させます 次に 発生の恐れがあるデータ競合の 安全対策シナリオについて説明し その対処方法を確認します 最後に 構造化並行処理を取り上げ TaskGroupなどのツールを使用して 並行処理コードをきめ細かく 制御する方法を紹介します 早速始めましょう! 私は日記をつけたり ページを ステッカーで飾りつけるのが大好きです 任意の写真セットから ステッカーパックを作成するための アプリの構築方法を説明します アプリには2つのメインビューがあります 最初のビューには オリジナルの写真の色を反映した グラデーションのステッカーが 入ったカルーセルが表示され 2番目のビューには ステッカーパック全体の グリッドプレビューが表示され エクスポートできます サンプルアプリをダウンロードして 一緒に始めてみましょう! プロジェクトを作成した際 Xcodeで並行処理の適用を 容易にするための 機能を有効にしました デフォルトのメインアクターモードなど 追加予定の新機能が含まれます これらの機能は Xcode 26の新規 アプリプロジェクトではデフォルトです
使いやすい 並行処理構成では Swift 6の言語モードは 準備ができるまで 並行処理を導入せず データ競合の安全性を確保します これらの機能を既存のプロジェクトで 有効にする場合は Swift移行ガイドで手順を確認できます
アプリのコードには StickerCarouselと StickerGridというメインビューがあります これらのビューは PhotoProcessor構造体が抽出する ステッカーを使用します
PhotoProcessorは ステッカーを返す前に フォトライブラリから 生の画像を取得します
StickerGridビューにはステッカーを 共有するためのShareLinkがあります
PhotoProcessor型は ステッカーの抽出と ドミナントカラー演算という 2つの負荷の高い演算を実行します Swiftの並行処理機能が スムーズなユーザー体験を実現しつつ デバイスで負荷の高いタスクを 実行するため 最適化する方法を 見ていきましょう! まず StickerCarouselビュー から始めます このビューでは ステッカーが 水平スクロールビューで表示されます スクロールビュー内には ForEachがあり ビューモデルに保存されている フォトライブラリから選択された 写真の配列を反復処理します viewModelのprocessedPhotos ディクショナリをチェックして フォトライブラリ内の選択に 対応する処理済みの写真を取得します ここでは 写真ピッカーから 画像を取得するための コードを実際に書いていないため 処理済みの写真はありません ここでアプリを実行すると スクロールビューに表示されるのは StickerPlaceholderビューのみです 「command」キーを押しながらクリックして StickerViewModelにナビゲートします StickerViewModelは フォトライブラリから 選択されている写真の配列を SelectedPhoto型として表示します この型は「option」キー+クリックで開く Quick Helpで詳細を確認できます
SelectedPhotoは PhotosUIフレームワークからの PhotosPickerItemとその関連IDを 格納する識別可能な型です このモデルには 選択された写真のIDを SwiftUIの画像にマッピングする 「processedPhotos」という名前の 辞書も含まれています ここでは 選択された写真を表示する loadPhoto関数の実装を 開始しています ただ ここでは保存されている 写真ピッカーからデータは読み込まれません PhotosPickerItemはSDKのTransferable プロトコルに準拠しているため asynchronous loadTransferable関数を 使用して 要求した表現を 非同期で読み込むことができます ここでデータ表現をリクエストします
コンパイラエラーが発生しました
「loadTransferable」呼び出しが非同期で それを呼び出す「loadPhoto」関数が 非同期呼び出しの処理用として 設定されていないからです そこで Swiftは 「loadPhoto」に 「async」キーワードを付けることを 提案しています この案を適用します
この関数は 非同期コードを処理できますが まだ エラーが残っています 「loadPhoto」は非同期呼び出しを処理 できますが 何を待機するかの指定が必要です そのためには 「loadTransferable」への 呼び出しを「await」キーワードで マークする必要があります この修正案を適応します
StickerCarouselビューで この関数を呼び出します 「command+shift+O」キーでXcodeの 「Open Quickly」を使用して StickerCarouselに戻ることができます
StickerPlaceholderビューが表示されたら loadPhoto関数を呼び出します この関数は非同期であるため このビューが表示されたら SwiftUIタスク修飾子を使用して 写真の処理を開始します
私のデバイスでチェックしてみましょう
いいですね 動いています 試しに写真を 何枚か選択してみましょう
いいですね画像は 私のフォトライブラリから 読み込まれていますね このタスクにより データから画像が 読み込まれている間も アプリのUIの応答性を維持できます 画像の表示にLazyHStackを 使用しているため画面でレンダリングが 必要なビューに対してのみ 写真の読み込みタスクを開始しています つまり アプリは必要以上の 作業を実行していません async/awaitがアプリの応答性を向上させる 理由について説明しましょう
「loadTransferable」メソッドを呼び出す際 「await」キーワードを追加し 「loadPhoto」関数に 「async」の注釈を付けました 「await」キーワードは可能性のある 一時停止ポイントをマークします つまり 最初にloadPhoto関数が メインスレッドで開始され awaitで loadTransferableを呼び出すと loadTransferableが完了するまでの 待機中は 一時停止になります loadPhotoが一時停止の間 Transferableフレームワークは バックグラウンドスレッドで loadTransferableを実行します loadTransferableが完了すると loadPhotoはメインスレッドで 実行を再開し 画像を更新します loadPhotoが一時停止の間 メインスレッドはUIイベントに応答し 他のタスクを実行できます 関数が一時停止されている間 awaitキーワードは 他の作業を実行できる コード内のポイントを示します これで フォトライブラリからの 画像の読み込みが完了しました その過程で 非同期コードとは何か どのように書き 考えるかを学びました 次に アプリにコードを追加して 写真からステッカーを抽出し プライマリカラーを抽出しますが このカラーはカルーセル表示で 背景のグラデーションに使用できます
「command」キーを押しながら クリックすると loadPhotoに戻り これらの効果を適用します
プロジェクトにはすでに PhotoProcessorが含まれており データを取得して色とステッカーを抽出し 処理済みの写真を返します データから 基本画像を提供するのではなく 代わりに PhotoProcessorを使用します
PhotoProcessorが 処理された写真を返すので 辞書の型を更新します
このProcessedPhotoは 写真から抽出されたステッカーと グラデーションを構成する 色の配列を提供します
プロジェクトにはすでに processedPhotoを 受け取るGradientStickerビューがあります 「Open Quickly」を使用して そこに移動します
このビューでは ZStackの線形 グラデーションで処理済みの写真に 保存されたステッカーが表示されます
このGradientStickerを カルーセルに追加します
現在 StickerCarouselでは 写真のサイズを変更しているだけですが 処理済みの写真があるので 代わりにGradientStickerを使用します
アプリを構築して実行し ステッカーを確認しましょう
うまく行っています
ああ ダメですね ステッカーを抽出している間の カルーセルのスクロールが ギクシャクしています
画像処理は 負荷が高いからでしょう これを確認するため Instrumentsで アプリのプロファイルを作成しました トレースには アプリに重大なハングが 発生していることが示されています
ズームインして 最も重い スタックトレースを確認すると 写真プロセッサが 負荷の高い処理タスクで 10秒以上もメインスレッドを ブロックしています! アプリにハング分析については セッション 「Instrumentsによるハング分析」を ご覧ください 次に このアプリがメインスレッドで実行する 作業について詳しくお話ししましょう
「loadTransferable」の実装では メインスレッドで読み込みが 発生しないように 作業をバックグラウンドに オフロードする処理が行われました
メインスレッドで実行され 完了まで時間を要する 画像処理コードを追加したので メインスレッドは スクロールジェスチャーへの応答など UI更新を受信できず アプリのユーザー体験が低下します
以前は SDKから非同期APIを 適用していたので 代行で作業がオフロードされていました 今度は ハングを修正するため 独自のコードを並列処理します 画像変換の一部を 背景に移動することができます 画像の変換は3つの演算で 構成されています 生の画像を取得し 画像を更新するには UIの操作が必要であるため この作業をバックグラウンドに 移動させることはできませんが 画像処理をオフロードすることはできます これで 負荷の高い画像処理が 行われている間 メインスレッドが他のイベントに 自由に応答できるようになります その方法を理解するには まず PhotoProcessor構造体を見てみましょう
このアプリはデフォルトで メインアクターモードであるため PhotoProcessorは @MainActorに関連づけられており すべてのメソッドはメインアクターで 実行する必要があります 「process」メソッドはextract stickerと extract colorsメソッドを呼び出すので この型の全メソッドをメインアクターから 実行可能としてマークする必要があります これを実行するには PhotoProcessor型 全体を非隔離としてマークします これは Swift 6.1で導入された 新機能です 型が非隔離としてマークされている場合 そのすべてのプロパティとメソッドは 自動的に非隔離になります
PhotoProcessorがMainActorに 関連付けられなくなったので プロセス関数に 新しい「@concurrent」属性を適用し 「async」でマークします これで Swiftがこのメソッドを実行する際は バックグラウンドスレッドに切り替わります 「Open Quickly」を使って PhotoProcessorに戻ります
まず 型にnonisolatedを適用して PhotoProcessorをメインアクターから分離し そのメソッドを並行コードから 呼び出せるようにします
PhotoProcessorが非隔離になったので バックグラウンドスレッドから プロセスメソッドが呼び出されるように @concurrentとasyncを適用します
ここで「Open Quickly」を使用して StickerViewModelに戻りましょう
Swiftが推奨するようにloadPhotoメソッドは 「await」キーワードを使用して processメソッドを呼び出すことで メインスレッドを終了する必要があります この案を適用します
アプリを構築して実行し この作業を メインアクターから移動することで ハングが軽減されたか 確認しましょう
スクロールのハングは 解消されたようです
ただ UIは操作できても スクロール中に 画像がUIに表示されるまで 時間がかかります ユーザー体験を向上させる要素は アプリの応答性だけではありません メインスレッドから作業を移動させても 結果が出るまで時間がかかるので ユーザーにとっては アプリで体験する イライラは解消されません
画像処理の演算は バックグラウンド スレッドに移しましたが それでも 完了まで かなり時間がかかります 並行処理でこの演算を最適化し 完了するまでの時間を 短縮できるかを見てみましょう 画像処理には ステッカーと ドミナントカラーの抽出が必要ですが この演算はお互いに 独立しています そこで async letを使用すれば これらのタスクを並列処理できます すべてのバックグラウンドスレッドを 管理する並行処理スレッドプールは 2つのタスクが別々のバックグラウンド スレッドで同時に開始するように設定します これで 携帯電話のマルチコアを 有効活用できます
「command」キーを押しながらクリックして async letを適用します
「control+shift」キーを押しながら下矢印で マルチラインカーソルを使用して ステッカーとカラー変数の前に asyncを追加します
この2つの呼び出しを 並列処理したので その結果を待って プロセス関数を再開します 「Editor」メニューを使用して 問題をすべて修復しましょう
まだ もう1つエラーがあります 今度はデータ競合です! 少し時間をとって このエラーについて 説明しましょう
このエラーが発生したのは 私のPhotoProcessor型は 並行処理タスク間の共有が 危険であることを意味します その理由を知るため ストアドプロパティを見てみましょう PhotoProcessorが保存する 唯一のプロパティは 写真から色を抽出するのに必要な ColorExtractorのインスタンスです ColorExtractorクラスは画像に表示される ドミナントカラーを計算します この計算は ピクセルバッファを含む 低レベルの可変画像データに対して 実行されるので color extractor型の 同時アクセスは危険です
すべての色抽出演算はColorExtractorの 同じインスタンスを共有します これにより 同じメモリへの 並行アクセスが発生します
これは「データ競合」と呼ばれ クラッシュや予期しない動作など ランタイムバグにつながります Swift 6の言語モードでは コンパイル時にこれを識別し 並列処理コードを記述する際に バグのセットを除外します これによって 難しいランタイムバグが すぐに対処できる コンパイラエラーに移行します エラーメッセージの ボタンクリックすると SwiftのWebサイトで このエラーの詳細を確認できます データ競合を解決する際に 検討するオプションは複数あります どれを選ぶかは コードが共有データを 使用する方法によって異なります まず 確認しましょう この変更可能な状態は並行処理コード間で 共有する必要があるでしょうか? ほとんどの場合 ただ共有を避けるだけですが ただし こうしたコードで状態の共有が 必要なケースもあります その場合は 共有する必要があるものを 安全に送信できる値の型に 抽出してみましょう これらの解決策を 適用できない場合のみ この状態をMainActorなどのアクターに 隔離することを検討してください 最初の解決策が このケースで有効か見てみましょう この型をリファクタリングして 複数の並列処理に 対応するように変更することもできますが 代わりに extractColors関数の 色抽出関数を ローカル変数に移動し 処理中の写真に独自の 色抽出関数のインスタンスを持たせます 色抽出関数が処理するのは 1回につき写真1枚であるため このコード変更は正しいですね つまり 色抽出タスクごとに 個別のインスタンスが必要です このコード変更により extractColors関数の外部からは 色抽出にアクセスできなくなるので データ競合が回避されます
この変更を行うには 色抽出プロパティを extractColors関数に移動します
いいですね コンパイラを活用し アプリのデータ競合を検出し 排除することができました では 実行してみましょう
アプリの動作が速く感じます!
Instrumentsでプロファイラ トレースを収集し 開くと もうハングはありません Swiftの並行処理による 最適化を簡単にまとめましょう 「@concurrent」属性を 適用することで 画像処理コードを メインスレッドから隔離できました また 「async let」を使用して ステッカーと色の抽出演算を 相互に並列化し アプリの パフォーマンスが向上しました Swiftの並行処理による最適化は タイムプロファイラインストルメントなど 分析ツールのデータに基づいて 行う必要があります 並行処理を適応しなくても コードを効率的にできる場合は 先にそれを実行してください アプリの動きがシャープですね! 画像処理は少し休憩して 楽しい要素を追加しましょう
処理済みのステッカーに 視覚効果を追加して ステッカーをスクロールすると フェードアウトしてぼやけるようにします Xcodeに切り替えて 書いてみましょう!
Xcodeプロジェクトナビゲータを使用して StickerCarouselに戻ります
ここで visualEffect修飾子を使用して スクロールビューの各画像に 視覚効果を適用します
ここでは ビューに エフェクトを適用します スクロールビューの最後のステッカーのみ オフセット、ぼかし、不透明度を 変更したいので 最後のステッカーに視覚効果が 適用されているか確認するには viewModelの選択プロパティに アクセスする必要があります
VisualEffectクロージャから メインアクターの保護されたビュー状態に アクセスしようとしているため コンパイラエラーが発生しています 視覚効果の計算は 負荷の高い演算であるため SwiftUIはアプリパフォーマンス向上のため 演算をメインスレッドからオフロードします
好奇心旺盛で もっと詳しく知りたいという方は セッション「SwiftUIの並行処理の詳細」を ご覧ください 以上のことがこのエラーからわかりました このクロージャは 後で バックグラウンドで評価されます これを「visualEffect」の定義を見て 確認しましょう 「command」キーを押しながら クリックします
定義では このクロージャは @Sendableです SwiftUIからの指示で このクロージャがバックグラウンドで 実行されることを示します
この場合 SwiftUIは選択が変更されると 視覚効果を再度呼び出すため クロージャのキャプチャリストを使用して コピーを作成できます
SwiftUIがこのクロージャを呼び出すと 選択値のコピーに演算が実行されるため この演算にはデータ競合が発生しません
視覚効果をチェックしてみましょう
きれいにできています スクロールすると 前の画像がぼやけて フェードアウトするのがわかります
ここまで見てきた2つの データ競合シナリオの解決策は 変更の可能性があるデータを 並行処理コードから共有しないことでした 両者の主な違いは コードの一部を並列処理することで データ競合を独自に導入したことです 2番目の例では SwiftUI APIを使用して バックグラウンドスレッドに 作業をオフロードしました
変更可能な状態を共有する場合は 他に保護する方法があります Sendable値の型は 並行処理の コード間で型が共有されるのを防ぎます たとえば extractStickerメソッドと extractColorsメソッドは並列処理され どちらも同じ画像のデータを取得します ただし データはSendable値型であり この場合は データ競合は発生しません データはコピーオンライトを実装しており 変更された場合のみコピーされます 値型を使用できない場合は 状態を メインアクターに隔離する方法もあります デフォルトモードでは メインアクターが すでにその処理を実行しています たとえば モデルはクラスであり 並行タスクからアクセスできます モデルは暗黙的に MainActorでマークされているので 並行処理コードから 参照しても安全です 状態にアクセスするには コードを メインアクターに切り替える必要があります この場合 クラスはメインアクターによって 保護されますが コード内の他のアクターにも 同じことが適用されます 今のところ アプリはいい感じです ただ まだ未完成です
ステッカーをエクスポートするには 未処理の写真ごとに 写真処理タスクを開始し すべてのステッカーを一括表示する ステッカーグリッドビューを追加します ステッカーをエクスポートできる 共有ボタンもあります では コードに戻りましょう
まず「command」キー+クリックで StickerViewModelに移動します
モデルに別のメソッド 「processAllPhotos()」を追加します
ここでは モデルに保存されている 処理済み写真をすべて反復処理し 未処理の写真がある場合は 一括処理を開始するための 複数の並列タスクを設定します
以前は async letを使用しましたが この場合は ステッカーと色抽出という タスクが2つのみであるため スムーズに機能したのです 配列内のすべての未処理写真に対して 新しいタスクを作成する必要がありますが 処理タスクは任意の数だけ存在します
TaskGroupなどのAPIを使用すると アプリで実行する必要のある 非同期作業を細かく管理できます
タスクグループは 子ども用タスクと その結果を細かく管理します タスクグループでは 並行処理できる 任意の数の子タスクを開始できます
それぞれの子タスクは 完了までに要する時間が異なるので 処理される順序が 異なる可能性があります この場合では 処理された写真は 辞書に保存されるため 順序は重要ではありません
TaskGroupは AsyncSequenceに準拠しているため 完了したら 結果を反復処理して 辞書に格納することができます 最後に グループ全体が 子タスクを完了するまで待機します コードに戻って タスクグループを適用してみましょう タスクグループを適用するには まず 宣言から始めます
クロージャには 画像処理タスクを追加できる グループへの参照があります モデルに保存された 選択範囲を反復処理します
この写真が処理済みなら タスクを作成する必要はありません
データの読み込みと写真の処理という 新しいタスクを開始します
グループは非同期シーケンスであるため 処理済みの写真の準備ができたら それを反復処理して processedPhotos辞書に 保存することができます
これで完了です これで StickerGridにステッカーが 表示されるようになります
「Open Quickly」を使用して StickerGridに戻ります
ここには すべての写真の処理が 完了したことを示す状態プロパティ 「finishedLoading」があります
写真が未処理の場合は 進捗状況ビューが表示されます 先ほど実装した processAllPhotos()メソッドを呼び出します
すべての写真が処理されたら 状態変数を設定できます 最後に ツールバーに共有リンクを 追加して ステッカーを共有します
共有リンクアイテムに選択済みの各写真の ステッカーを入力していきます
アプリを実行してみましょう!
StickerGridボタンをタップすると TaskGroupによりプレビューグリッドが 写真の一括処理を開始します 準備ができたら すべてのステッカーを一括で確認できます 最後に ツールバーのボタンを使えば すべてのステッカーを 保存可能なファイルとして エクスポートできます
このアプリでは ステッカーは 処理された順序で回収されます タスクグループには 順序をトラッキング するなどさまざまな機能があります 詳細は セッション「構造化並行処理の 基本を超えて」をご参照ください
おめでとう! アプリが完成したので これでステッカーを保存できます アプリに追加した機能とそれが UIに影響を与えるタイミングを把握し 応答性とパフォーマンスの向上に必要な 並行処理を使用しました また 構造化並行処理のほか データ競合を 防ぐ方法についても学びました
手順に従わなかった場合でも アプリの最終バージョンをダウンロードして 自分の写真から ステッカーを作成できます この動画で紹介した 新しいSwiftの 並行処理機能とテクニックに慣れるため アプリの最適化や調整を行ってください ここで紹介したテクニックを アプリに適用できるか試してみましょう まずは プロファイリングを行います! Swiftの並行処理モデルの概念を さらに深く理解するには セッション「Swiftの並列処理の活用」を ご覧ください 既存のプロジェクトを移行して 新しい並行処理機能を適用するには 「Swift移行ガイド」をご覧ください 今回 最高だったのは ノート用のステッカーができたことです! ご視聴ありがとうございました
-
-
6:29 - Asynchronously loading the selected photo from the photo library
func loadPhoto(_ item: SelectedPhoto) async { var data: Data? = try? await item.loadTransferable(type: Data.self) if let cachedData = getCachedData(for: item.id) { data = cachedData } guard let data else { return } processedPhotos[item.id] = Image(data: data) cacheData(item.id, data) }
-
6:59 - Calling an asynchronous function when the SwiftUI View appears
StickerPlaceholder() .task { await viewModel.loadPhoto(selectedPhoto) }
-
9:45 - Synchronously extracting the sticker and the colors from a photo
func loadPhoto(_ item: SelectedPhoto) async { var data: Data? = try? await item.loadTransferable(type: Data.self) if let cachedData = getCachedData(for: item.id) { data = cachedData } guard let data else { return } processedPhotos[item.id] = PhotoProcessor().process(data: data) cacheData(item.id, data) }
-
9:56 - Storing the processed photo in the dictionary
var processedPhotos = [SelectedPhoto.ID: ProcessedPhoto]()
-
10:45 - Displaying the sticker with a gradient background in the carousel
import SwiftUI import PhotosUI struct StickerCarousel: View { @State var viewModel: StickerViewModel @State private var sheetPresented: Bool = false var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: 16) { ForEach(viewModel.selection) { selectedPhoto in VStack { if let processedPhoto = viewModel.processedPhotos[selectedPhoto.id] { GradientSticker(processedPhoto: processedPhoto) } else if viewModel.invalidPhotos.contains(selectedPhoto.id) { InvalidStickerPlaceholder() } else { StickerPlaceholder() .task { await viewModel.loadPhoto(selectedPhoto) } } } .containerRelativeFrame(.horizontal) } } } .configureCarousel( viewModel, sheetPresented: $sheetPresented ) .sheet(isPresented: $sheetPresented) { StickerGrid(viewModel: viewModel) } } }
-
14:13 - Allowing photo processing to run on the background thread
nonisolated struct PhotoProcessor { let colorExtractor = ColorExtractor() @concurrent func process(data: Data) async -> ProcessedPhoto? { let sticker = extractSticker(from: data) let colors = extractColors(from: data) guard let sticker = sticker, let colors = colors else { return nil } return ProcessedPhoto(sticker: sticker, colorScheme: colors) } private func extractColors(from data: Data) -> PhotoColorScheme? { // ... } private func extractSticker(from data: Data) -> Image? { // ... } }
-
15:31 - Running the photo processing operations off the main thread
func loadPhoto(_ item: SelectedPhoto) async { var data: Data? = try? await item.loadTransferable(type: Data.self) if let cachedData = getCachedData(for: item.id) { data = cachedData } guard let data else { return } processedPhotos[item.id] = await PhotoProcessor().process(data: data) cacheData(item.id, data) }
-
20:55 - Running sticker and color extraction in parallel.
nonisolated struct PhotoProcessor { @concurrent func process(data: Data) async -> ProcessedPhoto? { async let sticker = extractSticker(from: data) async let colors = extractColors(from: data) guard let sticker = await sticker, let colors = await colors else { return nil } return ProcessedPhoto(sticker: sticker, colorScheme: colors) } private func extractColors(from data: Data) -> PhotoColorScheme? { let colorExtractor = ColorExtractor() return colorExtractor.extractColors(from: data) } private func extractSticker(from data: Data) -> Image? { // ... } }
-
24:20 - Applying the visual effect on each sticker in the carousel
import SwiftUI import PhotosUI struct StickerCarousel: View { @State var viewModel: StickerViewModel @State private var sheetPresented: Bool = false var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: 16) { ForEach(viewModel.selection) { selectedPhoto in VStack { if let processedPhoto = viewModel.processedPhotos[selectedPhoto.id] { GradientSticker(processedPhoto: processedPhoto) } else if viewModel.invalidPhotos.contains(selectedPhoto.id) { InvalidStickerPlaceholder() } else { StickerPlaceholder() .task { await viewModel.loadPhoto(selectedPhoto) } } } .containerRelativeFrame(.horizontal) .visualEffect { [selection = viewModel.selection] content, proxy in let frame = proxy.frame(in: .scrollView(axis: .horizontal)) let distance = min(0, frame.minX) let isLast = selectedPhoto.id == selection.last?.id return content .hueRotation(.degrees(frame.origin.x / 10)) .scaleEffect(1 + distance / 700) .offset(x: isLast ? 0 : -distance / 1.25) .brightness(-distance / 400) .blur(radius: isLast ? 0 : -distance / 50) .opacity(isLast ? 1.0 : min(1.0, 1.0 - (-distance / 400))) } } } } .configureCarousel( viewModel, sheetPresented: $sheetPresented ) .sheet(isPresented: $sheetPresented) { StickerGrid(viewModel: viewModel) } } }
-
26:15 - Accessing a reference type from a concurrent task
Task { @concurrent in await viewModel.loadPhoto(selectedPhoto) }
-
29:00 - Processing all photos at once with a task group
func processAllPhotos() async { await withTaskGroup { group in for item in selection { guard processedPhotos[item.id] == nil else { continue } group.addTask { let data = await self.getData(for: item) let photo = await PhotoProcessor().process(data: data) return photo.map { ProcessedPhotoResult(id: item.id, processedPhoto: $0) } } } for await result in group { if let result { processedPhotos[result.id] = result.processedPhoto } } } }
-
30:00 - Kicking off photo processing and configuring the share link in a sticker grid view.
import SwiftUI struct StickerGrid: View { let viewModel: StickerViewModel @State private var finishedLoading: Bool = false var body: some View { NavigationStack { VStack { if finishedLoading { GridContent(viewModel: viewModel) } else { ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } } .task { await viewModel.processAllPhotos() finishedLoading = true } .toolbar { ToolbarItem(placement: .topBarTrailing) { if finishedLoading { ShareLink("Share", items: viewModel.selection.compactMap { viewModel.processedPhotos[$0.id]?.sticker }) { sticker in SharePreview( "Sticker Preview", image: sticker, icon: Image(systemName: "photo") ) } } } } .configureStickerGrid() } } }
-