View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

WWDC25に戻る

  • 概要
  • トランスクリプト
  • コード
  • SwiftUIの並行処理の詳細

    SwiftUIで、Swiftの並行処理を活用して安全で応答性の高いアプリを構築する方法を紹介します。デフォルトでメインアクターを使用し、ほかのアクターにタスクをオフロードする、SwiftUIの機能について解説します。SwiftUIのイベントループを使用することで並行処理のアノテーションを解釈し、非同期タスクを管理して、スムーズなアニメーションやUIの更新を行う方法を学びます。セッションを通じて、データ競合を回避して安心してコードを記述する方法を理解できます。

    関連する章

    • 0:00 - イントロダクション
    • 7:17 - 「並行処理」の断崖
    • 16:53 - 「コード」キャンプ
    • 23:47 - 次のステップ

    リソース

    • Concurrency
    • Mutex
    • The Swift Programming Language: Concurrency
    • Updating an App to Use Swift Concurrency
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC25

    • Code Along:Swiftの並行処理によるアプリの強化
    • Swiftの並行処理の活用

    WWDC23

    • SwiftUIアニメーションの詳細
  • このビデオを検索

    皆さんこんにちは 私のツアーへようこそ 本日のガイド Danielです SwiftUIチームに所属しています

    今日は一緒に 並行処理の背景と SwiftUIによるアプリ開発について 見ていきたいと思います

    今日ご参加いただいたのは データ競合バグという危険な生き物の 話を耳にされたからだと思います

    これまでに何度か遭遇したことが あるかもしれません

    本日の話題は アプリの予期しない状態、 アニメーションの不具合、そして 永久的なデータ損失です

    このツアーは100%安全ですので ご心配なく SwiftとSwiftUIを使用すれば そのようなデータ競合アニマルを 置き去りにできます SwiftUIではさまざまな方法で コードを同時に実行できます このツアーでは SwiftUI APIの 並行処理の注釈を利用して それらのコードを識別する方法を解説します 最終的には皆さんがSwiftUIでの アプリ開発に自信を深め より大胆になれることを願っています

    Swift 6.2では新しい言語モードが 追加され このモードでは

    モジュール内のすべての型が 暗黙的に @MainActor注釈でマークされます

    このツアーで紹介する内容はすべて この新モードの利用にかかわらず有効です このツアーには 3つのアトラクションがあります

    まずメインアクターの 美しい牧草地から出発し SwiftUIでメインアクターが アプリのコンパイル時と実行時の デフォルトとなることを確認します

    次に待つのは 並行処理の崖です SwiftUIがアプリにどのように役立ち メインスレッドからタスクをオフロードして UIのヒッチを回避し 同時に 野生のデータ競合バグから 私たちを守るのかを見ていきます

    最後に キャンプ場に到着し 自分の位置付けを決めて 並列コード間の関係と SwiftUI APIを検討します

    では最初の目的地 メインアクターの牧草地に行きましょう

    ツアー中には 自然にインスパイアされた カラースキームを集めたいので そのためのアプリを作りました 写真を撮影したら 抽出したい色の数を選んで 抽出ボタンを押します するとアプリで 補完的な色が写真から抽出され 画面上に表示されます

    下にスクロールすると 抽出した カラースキームがすべて表示されるので 好きな色を選んで エクスポートできます

    抽出UIについては 構造体ColorExtractorViewを作りました

    これは @MainActorの隔離を宣言する SwiftUIのViewプロトコルに準拠しています

    Swiftはデータ隔離を使用して すべてのミュータブルステートの 安全性を理解し 検証します ツアー全体を通してこのような 多くの並行性の概念に遭遇するでしょう Swift Concurrencyを初めて使用する場合 または復習が必要な場合は 「Embracing Swift Concurrency」を ご覧ください SwiftUIでは@MainActorで Viewが隔離されるので 構造体をViewに適合させます

    このため ColorExtractorViewは @MainActorに隔離されます この点線は 推測された隔離であることを 示します つまり この注釈はコンパイル時に 暗示されますが 実際 私が書いたコードには 含まれていません

    @MainActorで 型全体が隔離される場合は そのすべてのメンバーが 暗黙的に隔離されます

    これにはViewの要件を実装する bodyプロパティが含まれ

    ここで宣言する他のメンバーも同様で この@State変数もそうです

    ビューのbodyを閉じます ここでは他のメンバープロパティ 例えば モデルのschemeや モデルの colorCountへのバインディングを参照しています

    これがコンパイラで許可されるのは 共有された@MainActorの隔離により これらのアクセスが安全であることが 保証されるからです

    また直感的でもあります

    @MainActorはSwiftUIのコンパイル時の デフォルトです つまり ほとんどの場合 アプリの機能の 構築に集中することができ 並行処理については あまり考える必要がありません

    並行処理の目的でコードに 注釈を付与する必要はなく 自動的に安全性が保たれます

    追加のコード用のスペースを確保するため これらの推測される隔離を 非表示にします

    @MainActorを使用した このコンパイル時のデフォルトは このビューでは同期コードを超えて 拡張されます

    データモデルの型には @MainActorの注釈は必要ありません

    モデルはビューの宣言内で インスタンス化しているので Swiftによって モデルインスタンスが 適切に隔離されます

    このSchemeContentViewには 色の抽出を 開始するための タップジェスチャが含まれています

    色抽出の機能は非同期なので それを呼び出すために Taskを使用して 非同期コンテキストに切り替えます

    ビュー本体は @MainActorに隔離されているため このタスクに付与したクロージャが メインスレッドでも実行されます これは本当に便利です

    @MainActor隔離はSwiftUIの コンパイル時のデフォルトです

    それにより ビューの記述が 便利にやりやすくなるだけでなく 他にも非常に実用的な理由があります AppKitとUIKitのAPIは 排他的に@MainActorに隔離されます

    SwiftUIは これらのフレームワークと シームレスにやり取りします 例えば プロトコルUIViewRepresentableは Viewプロトコルを微調整します 構造体と同様に これは@MainActor上で UIViewRepresentableを隔離します

    ですのでUIViewRepresentableに 準拠する型もViewです そのため @MainActorに隔離されます

    UILabelのイニシャライザは @MainActor隔離を要求します それはmakeUIViewで機能します なぜなら makeUIViewは @MainActor隔離である Representable型のメンバーだからです

    @MainActorを使用して注釈を 付ける必要はありません SwiftUIがそのAPIに @MainActorを使用して注釈を付けるのは 実装するデフォルトのランタイム動作が 反映されるからです

    このような注釈は実行時に フレームワークで意図された セマンティクスの下流になります

    SwiftUIの並行処理注釈は ランタイムセマンティクスを表します これはこれまでに見たコンパイル時の 便宜からすると 微妙な違いに思えるかもしれませんが 非常に根本的なことです この考えを裏付ける別の例を後で紹介します

    次のアトラクションは エキサイティングなものになります シートベルトを締め 電子機器が 固定されていることをご確認ください

    アプリの開発中に 多くのアプリ機能を導入すると メインスレッド内の処理が多すぎる場合 アプリでドロップフレームやヒッチが 発生することがあります タスクと構造化並行処理を使用すると メインスレッドから コンピューティング処理を オフロードできます 「Elevate an app with Swift Concurrency」セッションでは アプリのパフォーマンスを高めるための 実用的な手法をご紹介していますので ぜひご覧ください

    このツアーのメインテーマは SwiftUIでSwiftの並行処理を活用して アプリのパフォーマンスを高めることです

    SwiftUIチームは過去に 組み込みアニメーションでは バックグラウンドスレッドを使用して 中間状態が 計算されることを明らかにしています

    それをSchemeContentView内の この円で確認してみましょう

    色抽出ジョブの開始から終了までの間には 円が大きくなり そしてまた縮小して 元のサイズに戻る アニメーションが表示されます

    ここではプロパティisLoadingに反応する scaleEffectを使用しています

    このアニメーションの各フレームには 1から 1.5までの異なるスケール値が必要です

    このスケールのようなアニメーション値には 複雑な計算が含まれるため その多くをフレームごとに計算すると コストがかかる可能性があります そのためSwiftUIでは この計算を バックグラウンドスレッドで実行し メインスレッドには 他の処理のための 容量を残しておきます

    この最適化は 実装するAPIにも適用されます

    そうです SwiftUIではコードがメインスレッドから 実行されることがあるのです

    でもそれほど複雑ではないので 心配はいりません

    SwiftUIは宣言型です UIViewとは異なり Viewプロトコルに準拠する構造体は メモリ内の固定された場所を 占有するオブジェクトではありません

    実行時 SwiftUIでは ビューの個別の表現を作成します

    この表現により さまざまな種類の 最適化を実行できるようになります 中でも重要なのは バックグラウンドの スレッドでビューの表現の一部を 評価することです

    SwiftUIではこの技法を予約して 多くのコンピューティングが 実行される機会に備えます 例えば ほとんどの場合 この評価には 高頻度のジオメトリ計算が含まれます

    Shapeプロトコルはその一例です

    Shapeプロトコルには パスを返すメソッドが必要です

    ホイール内で抽出された色を表現するために カスタムのくさび形を作成しました

    そのパスメソッドを実装します

    くさび形はそれぞれ向きが異なります このくさび形が アニメーション化されている間 このパスメソッドでは 呼び出しが バックグラウンドスレッドから取得されます

    SwiftUIが自動的に実行する もう1つの カスタムロジックが クロージャ引数です

    円の真ん中に ぼやけたテキストが見えます

    これの実装には SwiftUIテキストで visualEffectを使用しています

    パルス値がtrueとfalseの間で 反転する際に ぼかし半径が2つの値の間で 切り替わるのです ビューモディファイアvisualEffectは サブジェクトビュー つまりテキストへの 効果を定義するクロージャを受け取ります 視覚効果は装飾的で レンダリングに コストがかかる場合があります

    SwiftUIはバックグラウンドスレッドから このクロージャを呼び出すことができます

    これがバックグラウンドスレッドから コードを呼び出せる2つのAPIです

    もう少し見てみましょう

    Layoutプロトコルは メインスレッドから 要件メソッドを呼び出すことができます また visualEffectと同様に onGeometryChangeの最初の引数は バックグラウンドスレッドからも 呼び出される可能性があるクロージャです

    バックグラウンドスレッドを使った このランタイム最適化は 長い間 SwiftUIの一部でした

    SwiftUIでは Sendable注釈を使用することで コンパイラやユーザーに対し このランタイム動作 つまりセマンティクスを表現できます

    ここでも SwiftUIの並行処理注釈は ランタイムセマンティクスを表します

    別のスレッドでコードを実行すると メインスレッドが解放され アプリの応答性が高まります

    そして Sendableキーワードは @MainActorのデータを共有する 必要があるときに データの競合状態が 生じる可能性があることを 思い出させるためのものです

    Sendableは 崖の斜面の小道にある 「危険 立入禁止」という 標識のようなものです

    でも この説明は少し大げさかもしれません 実際Swiftは コード内の 潜在的な競合状態を確実に検出し コンパイラエラーでそれを知らせます データ競合状態を回避する最善の戦略は 並行処理中のタスク間で データをまったく共有しないことです

    SwiftUI APIでSendable関数を 記述する必要がある場合 フレームワークは 関数の引数として必要な 変数のほとんどを提供します 簡単な例を示します

    先ほど説明しませんでしたが ColorExtactorViewでは カラーホイールとスライダーは同じ幅です それはこのEqualWidthVStack型のおかげです

    EqualWidthVStackはカスタムレイアウトです

    今回はレイアウトについては触れません ここでのポイントは SwiftUIによって渡される引数を使えば 外部変数を利用せずに このような 高度な計算を実行できるということです

    しかし Sendable関数の外部の変数に アクセスする必要がある場合はどうでしょう

    SchemeContentViewでは このvisualEffectに 状態pulseが必要です

    しかしSwiftでは データ競合状態の 警告が表示されます

    双眼鏡で コンパイラエラーの内容を 詳しく確認してみましょう

    pulse変数はself.pulseの略です これはSendableクロージャで @MainActor隔離変数を共有する場合の 一般的なシナリオです

    selfはViewで メインアクターで隔離されています ここから始めましょう 最終目標は Sendableクロージャに含まれる pulse変数にアクセスすることです これを達成するために 必要なことが2つあります

    まず 値selfはメインアクターから バックグラウンドスレッドの コード領域へと 境界を越えて移動する必要があります

    Swiftではこのことを「変数selfを バックグラウンドスレッドに 送信する」と言います

    これには selfの型が Sendableである必要があります

    selfが正しい場所に現れたので そのプロパティpulseをこの非隔離領域で 参照したいと思います でもコンパイラは プロパティpulseが どのアクターにも隔離されないのでない限り それを許可しません

    コードをもう一度見てみると selfはViewであるため @MainActorによって保護されており

    コンパイラには Sendableと見なされます

    そのためSwiftは この参照が @MainActor隔離から Sendableクロージャへと 境界を越えて移動することを 問題なく受け入れます

    そのためSwiftは実際 pulseプロパティに アクセスする試みに対して警告しています

    もちろん pulseがViewのメンバーとして MainActorに 隔離されていることはわかっています

    そのためコンパイラは selfをここに送信できても @MainActorに隔離された pulseプロパティへのアクセスは 安全でないことを伝えています

    このコンパイルエラーを 修正するには Viewへの参照を通じた プロパティの読み取りを避けます

    ここで記述した視覚効果には このView全体の値は必要なく pulseがtrueかfalseかを 知りたいだけです ですので代わりに クロージャの キャプチャリストでpulse変数の コピーを作成し それを参照します そうすれば このクロージャに selfを送信する必要はありません

    pulseのコピーを送信するだけです Boolは単純な値型なので このコピーは送信可能です

    このコピーは この関数の範囲内にのみ 存在するため アクセスしても データ競合の問題は発生しません

    この例では Sendableクロージャの pulse変数は グローバルアクターによって 保護されているため アクセスできませんでした

    これを機能させる別の戦略は 参照するすべてのものを 非隔離状態にすることです

    さて皆さん キャンプ場に到着しました ここからは並行コードの 整理方法について説明します

    経験豊富なSwiftUIデベロッパなら ボタンのアクションコールバックを含む 大半のSwiftUIのAPIが 同期的であることにお気付きでしょう

    並行コードを呼び出すには まず Taskで非同期コンテキストに 切り替える必要があります

    でもButtonはなぜ非同期クロージャを 受け入れないのでしょうか

    同期的な更新は 優れたユーザー体験を 実現するために重要です それが特に重要なのは アプリに実行時間の長いタスクがある場合や ユーザーが結果を待つ必要がある場合です

    async関数を使用して 実行時間の長いタスクを開始する前に UIを更新し タスクが進行中であることを 示すことが重要です

    この更新は同期である必要があります 特に時間が重要なアニメーションを トリガーする必要がある場合はなおさらです

    例えば 言語モデルに色の抽出を サポートしてもらうとします その抽出プロセスには少し時間がかかります このアプリでは withAnimationを使用して さまざまな読み込み状態を 同期的にトリガーします

    タスクが完了したら 別の同期状態の変化を利用して これらの読み込み状態を逆にします

    SwiftUIのアクションのコールバックは 読み込み状態などの UIの更新を設定するために必要な 同期的なクロージャを受け入れます

    一方 非同期関数は 特別な考慮が必要です 特にアニメーションを処理する場合は なおさらです それでは それを見ていきましょう

    このアプリでは 上にスクロールして 以前のカラースキームの履歴を 確認できます 各カラースキームが画面に表示されるときに その色をアニメーションで表現するとします

    onScrollVisibilityChange ビューモディファイアを使うと カラースキームが画面に表示されたときに イベントが発生します それが発生したらすぐに 状態変数をtrueに設定して アニメーションを トリガーすると 各色のYオフセットが アニメーションで更新されます

    SwiftUIが UIフレームワークとして 毎フレームでスムーズな操作を実現するには 各デバイスに特定の 画面リフレッシュレートが 必要だという現実に向き合わねばなりません

    それが重要になるのは 例えば スクロールのような継続的な ジェスチャーにコードを 反応させたいときです このコードをタイムラインに配置します

    この緑色の三角形を使用して SwiftUIがonScrollVisibilityChangeを 呼び出す瞬間をマークします 青い円は 状態の変化によって アニメーションをトリガーする 瞬間を示しています

    この設定では その変化がジェスチャーのコールバックと 同じフレームで起こるかどうかによって 視覚的に大きな違いが生まれます

    変化のアニメーションの前に 非同期処理をいくつか追加します 非同期処理が開始された瞬間を オレンジ色の線でマークして待機します

    Swiftでは 非同期関数待機すると 一時停止ポイントが作成されます

    Taskは同期関数を 引数として受け入れます

    コンパイラは awaitを検出すると 非同期関数を2つに分割します

    最初の部分を実行した後 Swiftランタイムはこの関数を一時停止し CPUで他の処理を行うことができます これは任意の時間 継続できます その後ランタイムは 元の非同期関数で再開し 残りの部分を実行します

    このプロセスは 関数でawaitが 発生するたびに繰り返すことができます

    タイムラインに戻ると この一時停止によって タスクのクロージャがいつまでも再開されず

    デバイスに指示された更新期限を 過ぎてしまう可能性があります

    ユーザーには アニメーションの動きが遅く ずれているように見えることになります このため 非同期関数の変更は 目標達成の役に立たない可能性があります

    SwiftUIでは デフォルトで 同期コールバックが提供されます これにより 非同期コードの 意図しない中断を回避できます 同期アクションクロージャ内のUIの更新は 簡単に正しく行うことができます Taskを使用すると いつでも 非同期コンテキストにオプトインできます

    アニメーションのように 時間が重要なロジックでは SwiftUIの入力と出力が 同期している必要があります 監視可能なプロパティの同期的な変更と 同期コールバックは フレームワークとの 最も自然なインタラクションです 優れたユーザー体験の実現に 多くのカスタム並行ロジックは不要です 同期コードは 多くのアプリにとって 優れた出発点であり 終着点でもあります

    一方 アプリが多くの並行処理を行う場合は UIコードと非UIコードの間の境界に 注目してみてください

    非同期処理のロジックとビューロジックを 隔離することが最適です

    状態の一部を橋渡しとして使用できます 状態により UIコードが非同期コードから 切り離されます

    すると非同期タスクが開始されます

    一部の非同期処理が終了したら 状態に対して同期的な変更を実行します その変更に対する反応として UIが更新されます

    このように UIロジックはほぼ同期的です

    別の利点として 非同期コードのテストを 簡単に記述できるようになります このコードは UIロジックから 独立しているからです

    ビューでは非同期コンテキストへの 切り替えをTaskで行えますが

    この非同期コンテキストの コードはシンプルに保ってください ここで UIイベントについて モデルに通知します

    時間が重要な変更が 多く必要になるUIコードと 実行時間の長い非同期ロジックとの 境界を見つけることは アプリの構造を改善するための 優れた方法です

    これは ビューの同期と応答性を 維持するのに役立ちます また 非UIコードを 適切に整理することも重要です

    そうした作業の自由度を広げるため 今回ご紹介したヒントをぜひご活用ください

    Swift 6.2は 優れたデフォルトの アクター隔離設定を備えています 既存のアプリをお持ちの場合は ぜひお試しください ほとんどの@MainActor注釈は 削除できるようになります

    ミューテックスは クラスを送信可能にする 重要なツールです 使用方法については 公式ドキュメントを確認してください

    ぜひアプリで非同期コードの ユニットテストを記述してみてください SwiftUIをインポートせずに できるかどうかお試しください

    それではみなさん 以上が SwiftUIで Swiftの並行処理を活用して データ競合が発生しない高速アプリを 構築する方法になります

    このツアーにご参加いただいたことで SwiftUIの並行処理に関して 確かな メンタルモデルが得られたかと思います

    ご視聴ありがとうございました ぜひ素晴らしい旅をお続けください

    • 2:45 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorScheme: Identifiable, Hashable {
          var id = UUID()
          let imageName: String
          var colors: [Color]
      }
      
      @Observable
      final class ColorExtractor {
          var imageName: String
          var scheme: ColorScheme?
          var isExtracting: Bool = false
          var colorCount: Float = 5
      
          func extractColorScheme() async {}
      }
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 5:55 - AppKit and UIKit require @MainActor: an example

      // AppKit and UIKit require @MainActor
      // Example: UIViewRepresentable
      
      struct FancyUILabel: UIViewRepresentable {
          func makeUIView(context: Context) -> UILabel {
              let label = UILabel()
              // customize the label...
              return label
          }
      }
    • 6:42 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorScheme: Identifiable, Hashable {
          var id = UUID()
          let imageName: String
          var colors: [Color]
      }
      
      @Observable
      final class ColorExtractor {
          var imageName: String
          var scheme: ColorScheme?
          var isExtracting: Bool = false
          var colorCount: Float = 5
      
          func extractColorScheme() async {}
      }
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractorModel()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack(spacing: 30) {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 8:26 - Animated circle, part of color scheme view

      // Part of color scheme view
      
      struct SchemeContentView: View {
          let isLoading: Bool
          @State private var pulse: Bool = false
      
          var body: some View {
              ZStack {
                  // Color wheel …
      
                  Circle()
                      .scaleEffect(isLoading ? 1.5 : 1)
      
                  VStack {
                      Text(isLoading ? "Please wait" : "Extract")
      
                      if !isLoading {
                          Text("^[\(extractCount) color](inflect: true)")
                      }
                  }
                  .visualEffect { [pulse] content, _ in
                      content
                          .blur(radius: pulse ? 2 : 0)
                  }
                  .onChange(of: isLoading) { _, newValue in
                      withAnimation(newValue ? kPulseAnimation : nil) {
                          pulse = newValue
                      }
                  }
              }
          }
      }
    • 13:10 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 13:47 - Part of color scheme view

      // Part of color scheme view
      
      struct SchemeContentView: View {
          let isLoading: Bool
          @State private var pulse: Bool = false
      
          var body: some View {
              ZStack {
                  // Color wheel …
      
                  Circle()
                      .scaleEffect(isLoading ? 1.5 : 1)
      
                  VStack {
                      Text(isLoading ? "Please wait" : "Extract")
      
                      if !isLoading {
                          Text("^[\(extractCount) color](inflect: true)")
                      }
                  }
                  .visualEffect { [pulse] content, _ in
                      content
                          .blur(radius: pulse ? 2 : 0)
                  }
                  .onChange(of: isLoading) { _, newValue in
                      withAnimation(newValue ? kPulseAnimation : nil) {
                          pulse = newValue
                      }
                  }
              }
          }
      }
    • 17:42 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 18:55 - Animate colors as they appear by scrolling

      // Animate colors as they appear by scrolling
      
      struct SchemeHistoryItemView: View {
          let scheme: ColorScheme
          @State private var isShown: Bool = false
      
          var body: some View {
              HStack(spacing: 0) {
                  ForEach(scheme.colors) { color in
                      color
                          .offset(x: 0, y: isShown ? 0 : 60)
                  }
              }
              .onScrollVisibilityChange(threshold: 0.9) {
                  guard !isShown else { return }
                  withAnimation {
                      isShown = $0
                  }
              }
          }
      }

Developer Footer

  • ビデオ
  • WWDC25
  • SwiftUIの並行処理の詳細
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン