View in English

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

クイックリンク

5 クイックリンク

ビデオ

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

WWDC25に戻る

  • 概要
  • トランスクリプト
  • コード
  • InstrumentsによるSwiftUIのパフォーマンス最適化

    新しいSwiftUIのInstrumentsを紹介します。SwiftUIによるビュー更新の仕組み、更新がアプリのデータに及ぼす影響、それらの影響の原因と結果を新しいInstrumentsで視覚化する方法について説明します。 このセッションの内容を十分理解できるよう、SwiftUIによるアプリの記述を理解しておくことをおすすめします。

    関連する章

    • 0:00 - イントロダクションとアジェンダ
    • 2:19 - SwiftUIのInstrumentsの紹介
    • 4:20 - 長時間を要するビュー本体の更新の診断と修正
    • 19:54 - SwiftUIによる更新の原因と結果の理解
    • 35:01 - 次のステップ

    リソース

    • Analyzing the performance of your visionOS app
    • Improving app responsiveness
    • Measuring your app’s power use with Power Profiler
    • Performance and metrics
    • Understanding and improving SwiftUI performance
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC25

    • InstrumentsによるCPUのパフォーマンス最適化

    WWDC23

    • Instrumentsによるハング分析
    • SwiftUIのパフォーマンスを解明
    • SwiftUIアニメーションの詳細

    WWDC22

    • SwiftUIによるカスタムレイアウトの作成

    WWDC21

    • SwiftUIの徹底解説

    Tech Talks

    • UIアニメーションの滞りおよびレンダーループに対する検討
  • このビデオを検索

    こんにちはInstrumentsチームのJedと Apple MusicチームのStevenです 良いアプリはパフォーマンスが優れています アプリでは 実行するどのコードも 遅延の原因になる可能性があります アプリを分析して コードのボトルネックと なり得る部分を特定し 問題を解決して アプリの円滑な動作を 確保することが重要です 本日のセッションでは SwiftUIコードにボトルネックがある場合に それを特定する方法を詳しく説明します また SwiftUIの動作を 効率化する方法も紹介します そもそも どうすれば 問題を認識できるのでしょうか 気付かれやすい症状の1つに ヒッチやハングが原因で アプリの反応が遅いことがあります 映像の一時停止やコマ落ち スクロール動作の遅延などです パフォーマンス問題を特定する 最もよい方法は Instrumentsによるプロファイリングです 今日は SwiftUIを使用したコードの パフォーマンス問題を 診断する方法を詳しく説明します まずは Instruments 26に含まれている 新しい SwiftUI Instrumentを紹介します 次に ビュー本体の更新が長い アプリについて確認し それがなぜ一般的な問題なのかを 説明してから Instrumentを使って それを発見し 修正します 最後に SwiftUIの更新の理由と その影響について詳しく説明します Instrumentを使って 不要な更新を特定し それらをなくす方法をお見せします パフォーマンスの問題には さまざまな根本原因が考えられますが ここでは SwiftUIの使い方が 原因であるものに的を絞ります

    アプリの問題がSwiftUIコードと 無関係なものである場合は 事態を把握する出発点として 「Instrumentsによるハング分析」と 「Instrumentsによる CPUのパフォーマンス最適化」の 視聴をお勧めします

    私達は共同で あるアプリを開発しています Steven 現時点での成果を 見せてもらえますか? 了解です 名前は「Landmarks」です このアプリでは 世界各地の魅力的な名所を 取り上げています それぞれのランドマークには 現在地からの距離が表示されるので 次にどの目的地に向かうのかや 飛行機での長旅になるのか 車で簡単に行けるのかなど 想像を巡らすことができます アプリは今のところ良さそうですが テストしていたところ スクロールが思った通りに 動作しない時があるのに気づきました その原因を突き止めたいと思います Jed 先ほど話していた 新しいSwiftUI Instrumentを 紹介してもらえますか はい Instruments 26には SwiftUIアプリのパフォーマンス問題を 特定するための新しい手段が 追加されました それが次世代のSwiftUI Instrumentです

    更新されたSwiftUIテンプレートには アプリのパフォーマンス評価に役立つ いくつかのInstrumentが含まれています まず 新しいSwiftUI Instrumentがあります これについては後で詳しく説明します

    次に Time Profilerです これは アプリがCPUで実行している 作業のサンプルを経時的に表示します 後はハングとヒッチの 各Instrumentがあります これらはアプリの応答性を トラッキングします

    アプリで潜在的なパフォーマンスの 問題を調査する際は まず SwiftUI Instrumentから得られる トップレベルの情報を確認します

    SwiftUI Instrumentトラックの 最初のレーンは「Update Groups」で SwiftUIが動作している期間を示します

    このレーンが空いている期間に CPU使用率が急上昇している場合は 問題がSwiftUIの外にある 可能性が高いことがわかります SwiftUI Instrumentトラックの 他のレーンでは SwiftUIの長い更新と その発生時期を容易に特定できます

    は ビューの「body」プロパティの実行時間が 長すぎる時を示しています では 時間が 長すぎるビューとビューコントローラの 表示可能な更新が特定されます

    は 前2つ以外の SwiftUIの長い動作を示しています パフォーマンス低下を生じさせている 可能性がある すべての長い更新の概要を これら3つのレーンで把握できます 更新はオレンジと赤で表示されます これはヒッチやハングに寄与している 可能性に応じた色付けです これらの更新が実際にアプリに ハングやヒッチを発生させるかどうかは デバイスの状態によりますが 長い更新を調べること そして 赤の更新をまず確認することが 通常は最適な出発点となります

    SwiftUI Instrumentを使用するには まずXcode 26をインストールします 次に アプリの実行と プロファイリングを行うデバイスを最新の OSリリースにアップデートします 最新リリースはSwiftUIトレースの 記録に対応しています Landmarksアプリはもう プロファイリングできると思います -Steven 始めてください -ありがとう Jed

    プロジェクトはすでにXcodeで開いています command + Iキーでプロファイリングを 開始すると アプリがリリースモードで コンパイルされ Instrumentsが自動で起動します

    テンプレートの選択画面で SwiftUIテンプレートを選びます

    記録ボタンをクリックして 記録を開始します

    まず ランドマークの一覧を スクロールします 大陸ごとに水平方向のシェルフがあります

    北米のシェルフの端まで横にスクロールして ビューを追加で読み込みます

    記録の停止をクリックします

    記録が停止された後 Instrumentsはデバイスから プロファイリングデータをコピーし 分析用に処理します 処理が完了したら SwiftUI Instrumentを使用して 注目すべき潜在的な パフォーマンス問題があるかどうかを 判断することができます すべての内容が見えやすいように ウインドウを最大化します

    まずは SwiftUI Instrument トラックにあるトップレベルの 長い更新レーンを調査します

    実行時間が長すぎるビュー本体は SwiftUIでパフォーマンス問題の 原因になることが多いため レーンを最初に調べます

    オレンジと赤の長い更新が いくつかあるので それらを調査したいと思います SwiftUIトラックをクリックして拡大します

    3つのサブトラック 、 、 が表示されます 各サブトラックでも トップレベルの レーンと同様に長い更新が オレンジと赤で表示されます 残りの更新はグレーで表示されます トラックを選択します

    下の詳細ペインには プロファイリングセッション中に 実行されたすべての ビュー本体の階層型の要約があります 階層内で私のアプリの プロセスを展開すると 実行されたすべてのビュー本体の更新に 関するモジュールが一覧表示されます

    ドロップダウンをクリックし の 要約を選択することで 長い更新だけに 絞り込むことができます

    数から 調査が必要な長い更新が いくつかあることがわかります

    モジュールをクリックして展開します LandmarkListItemViewに長い更新が いくつかあるため このビューから始めます

    ビュー名にカーソルを合わせると 矢印が表示されます

    それをクリックすると コンテキストメニューが開きます を選択します すると このビューの本体の長い更新が 順番に記載された一覧が表示されます

    長い更新の1つを右クリックし をクリックします

    このビュー本体の更新の間隔が トレース内の選択範囲として設定されます 次に Time Profiler Instrument トラックをクリックします

    ここでは ビュー本体の実行中にCPUで 起きていることを確認できます

    Time Profilerは定期的に CPUでの実行内容をサンプリングして データを収集します 各サンプルについて どの関数が実行中かチェックし その情報を プロファイリングセッションに 保存するのです 下のプロファイルの詳細ペインには トレース中に記録されたサンプルの コールスタックが表示されます この例では ビュー本体の実行中に 記録されたサンプルになります

    Optionキーを押しながらクリックし 主スレッドのコールスタックを展開します

    SwiftUIの動作が非常に 深いコールスタックで表されています ここで最も興味深いのは LandmarkListItemViewです コールスタックを検索するため command + Fキーを押し 検索フィールドに 名前を入力します

    ビュー本体がありました 左端の列に コールスタックの 各フレームに費やされた時間が Time Profilerによって表示されます

    この列を見ると ビュー本体に 費やされた時間のほとんどは distanceという 計算プロパティ内だったようです distanceで最も重い2つのフレームは 2つの異なるフォーマッタへの呼び出しです

    この測定フォーマッタと この数値フォーマッタです Xcodeに戻り コードで何が 起こっているのかを確認しましょう

    これはLandmarkListItemViewで 一覧内の各ランドマークの ビューです

    そしてこれがTime Profilerで気づいた distanceプロパティです このプロパティは ランドマークからの距離を ビューでの表示用に 書式設定された文字列に変換します

    これが数値フォーマッタです Time Profilerで見て 作成の負荷が高かったものです

    こちらは文字列を作成する 測定フォーマッタです これもビュー本体に長い時間が費やされる 大きな要因となっていました

    ビュー本体では ラベルテキストの作成のために distanceプロパティを読み取っています これはビュー本体の 実行のたびに行われます ビュー本体は主スレッドで 実行されるため アプリは距離のテキストが 書式設定されるのを待ってから UIの更新を続行します

    なぜこれが重要かというと ビュー本体の実行時間が1ミリ秒というのは 長くないように思えますが 積み重なると長い時間になる 可能性があり 特に画面上の多数のビューを更新する場合は それが顕著になるからです SwiftUIがビュー本体を実行する 時間については どのように 考えればいいでしょう いい質問です まず Appleプラットフォームでの レンダリングループの動作を説明します フレームごとに アプリが起動して タッチやキープレスなど イベントを処理します そしてUIを更新します その際 変更されたSwiftUIビューの bodyプロパティを実行します この作業は各フレームの期限までに すべて完了する必要があります アプリは作業をシステムに引き渡し システムは次のフレームの期限までに ビューをレンダリングします そしてレンダリングされた出力は その期限が来るとすぐに画面に表示されます ここではすべてが 適切に機能しています 更新は対応するフレームの 期限が来る前に完了するため システムが各フレームをレンダリングして 画面上に表示するのに十分な時間があります これを ビュー本体に時間がかかりすぎて ヒッチが生じたアプリと比較してみましょう

    前と同様 先にイベントを処理してから UIの更新を実行します しかし この最初のフレームで UIの更新に時間がかかりすぎました そのため UIの更新部分が フレームの期限を過ぎて実行されました ということは 次の更新は 後のフレームまで開始できません このフレームでは期限の時点でレンダラーに 作業を引き渡す準備ができていません

    その結果 次のフレームの レンダリングが完了するまで 画面に 前のフレームが表示されたままになります 画面への表示時間が 長すぎるフレームを呼び出すと 後のフレームが遅延し ヒッチが生じます ヒッチは映像の滑らかさを減少させます ヒッチの詳細については 「Understanding hitches in your app」の 記事をご覧ください また こちらのTech Talkでも レンダリングループと 各種ヒッチの修正方法を 詳しく説明しています ビュー本体の実行時間が重要な 理由の理解に役立ったでしょうか ええ とても役に立ちました ビュー本体の更新の実行に 必要以上の時間がかかる場合 アプリがフレームの期限を守れず ヒッチが発生するリスクがあるんですね そうすると 本体の実行中ではなく ビューを表示する前に ランドマークごとに距離の文字列を計算し キャッシュする方法が必要になります ではコードに戻りましょう

    こちらは ビュー本体の更新のたびに 実行されるdistanceプロパティです この作業はビュー本体の実行中に 行わないようにして より一元的な場所に 移動させることにします 移動先は位置の更新を管理するクラスです

    LocationFinderクラスには 位置変更のたびに 更新を受け取る役割があります 書式設定された距離の文字列は ビュー本体で計算するかわりに 事前に作成して ここにキャッシュする ことができるので ビューが距離の文字列を 表示するときには いつでも 計算が済んでいます

    まず イニシャライザを更新して 前にビュー本体で作成していた フォーマッタを作成します

    測定フォーマッタを格納するため formatterプロパティを追加しました

    イニシャライザの上部に 前にビューで作成していた 数値フォーマッタを作成します

    また測定フォーマッタも作成し 新しく追加したプロパティに格納します フォーマットは変わらないため 距離の文字列の更新が必要なときには 常にフォーマッタを再利用すれば ビュー本体の実行時に毎回 新しいフォーマッタを作り直す コストを防止できます 次に 必要なときにビューで 文字列を使用できるよう 文字列をキャッシュしておく手段が必要です これらの更新を管理するための コードを追加します

    ランドマークを格納する配列があります これは距離の計算に使います

    距離の文字列を 計算後にキャッシュするための 辞書も用意しました

    このupdateDistances関数は 位置の変更のたびに 文字列を再計算します

    ここでフォーマッタを使用して 距離のテキストを作成し

    ここでテキストをキャッシュに格納します

    そしてすぐに この最後の関数を呼び出して キャッシュ済みの テキストを取得します

    最後にもう1つ必要なのは 位置が更新されたら 文字列のキャッシュを更新することです

    Jump Barのドロップダウンをクリックし

    didUpdateLocations関数に 移動します これは位置変更の際に CoreLocationが呼び出す関数です

    ここで 前に作成した updateDistances関数を呼び出します

    ではビューに戻ります

    ビューを更新して キャッシュ済みの値を使用します

    以上の変更でビュー本体の 更新の遅延を修正できたはずです

    改善されたことを確認するために 修正を実装してから記録した Instrumentsトレースを見てみましょう

    トラックを選択すると 詳細ペインの の要約で LandmarkListItemViewへの長い更新が なくなったことがわかります

    要約には長いビュー本体の更新が まだ2つありますが それらの更新は トレースのごく初期に 発生しています これはアプリが 最初のフレームのレンダリングを 準備しているからです アプリ起動直後の更新に 時間がかかるのは珍しくありません システムがアプリの 初期ビュー階層をビルドしますが これはヒッチを招きません ここで重要な点は スクロール時のヒッチの原因に なっていた可能性がある LandmarkListItemViewの 長い更新が修正され 一覧から消えたことです つまり すべてのビューの表示が 正常に機能しているので SwiftUIの遅延はないと確認できます

    ビュー本体の長い更新の修正は アプリのパフォーマンスを高める 良い方法の1つです しかし 他にも 考慮すべきことがあります ビュー本体の更新が無駄に多い場合も パフォーマンスの問題が発生します 理由を探ってみましょう

    これは前にJedが紹介した図ですが 今回は 他の更新よりも長くかかった更新が 1つあるのではなく 比較的高速な更新がたくさんあり それらをすべて このフレーム内で実行する必要があります

    この余分な作業により アプリが期限内に フレームを送信できなくなっています 前と同様 次の更新は1フレーム分遅れます レンダラーに引き渡すものがないため またヒッチが発生します 前のフレームが2フレームにわたって 表示され続けるからです 無駄なビュー更新がパフォーマンスに及ぼす 影響についてお話しするのは それが今取り組んでいるアプリの 新しい機能に 大きく影響しそうだからです すべてのランドマークを スクロールするのは いろいろな場所を見れて楽しいのですが 行き先の優先順位を付けるのに苦労します そこで それを簡単にする アイデアを考えました ご覧ください 各ランドマークに新しく ハートボタンを追加しました タップでお気に入りへの 追加と削除ができます

    コードを見てみましょう

    LandmarkListItemViewに 新しいハートボタンを表示する オーバーレイを追加しました

    ボタンのアクションによって モデルデータクラスの toggleFavorite関数が呼び出され お気に入りへの追加/削除が行われます

    ランドマークのアイコンのハートは お気に入りの場合は塗りつぶされ そうでない場合は空になります

    commandキーを押しながらtoggleFavoriteを クリックし この関数に移動します

    お気に入りを追加する方法はこうです

    このモデルにお気に入りの ランドマークの配列を 格納し お気に入りが追加されたら ランドマークを配列に追加します

    お気に入りを削除する場合は 逆の処理を行います

    現時点ではこのような状態です もちろん さらに作業が必要ですが 開発中にInstrumentsで 早くから頻繁に プロファイリングしておくのはいいことです では 新機能のパフォーマンスを 確認してみましょう command + Iキーを押してアプリをビルドし Instrumentsに切り替えて

    記録ボタンをクリックします

    そして前と同様 北米のリストまで 下にスクロールし

    右にスライドして

    ハートをタップし ミュアウッズをお気に入りにします 家からそれほど遠くなく それでいて 行ったことがない場所だからです 今度は上にスクロールして 遠く離れた場所を お気に入りにしてみましょう 富士山はどうでしょう きっと楽しい旅になります ここで記録を停止します

    新しいお気に入りボタンをタップしても 不要な更新が余分に 発生しないことを 確認したいと思います 自分が予想する事態を念頭において トレースを分析し 予想から外れているものを探すことが 潜在的な問題を特定するうえで 効果的です

    SwiftUI Instrumentトラックを クリックして展開し サブトラックを選びます

    ミュアウッズと富士山の2つの お気に入りボタンをタップしたので この2つのビューが更新されたと予想します ボタンは下までスクロールした後に タップしており トレースの後半に記録されています 関心のある部分だけに注目するため トレースの後半をハイライトします

    下にある詳細ペインを確認します 階層を展開して ビューの更新の一覧を表示します

    驚いたことに LandmarkListItemViewは かなりの回数更新されていました なぜでしょうか UIKitアプリでビューの更新を デバッグする場合は 通常 コードにブレークポイントを設定し バックトレースを調べて ビューの更新理由を解明します しかしLandmarksなどのSwiftUIアプリでは うまく行きませんでした SwiftUIのコールスタックは わかりにくいようです Jed この手法がSwiftUIアプリで うまくいかないのはなぜでしょうか 説明します Xcodeではブレークポイントで バックトレースが表示されるため UIKitアプリの 命令型コードの原因と結果を把握できます UIKitは命令型フレームワークのため 原因と結果のデバッグには 多くの場合 バックトレースが役立ちます ここで ラベルが更新されている ことがわかります viewDidLoadのisOnプロパティを 設定しているからです バックトレース内のいくつかの システムフレームの名前から推測するに これはアプリが最初のシーンを 起動している間に起こったようです 同じ内容を実行する類似の SwiftUIアプリと比較すると SwiftUI内に再帰的な更新が いくつかあるのがわかります これらの更新はフレームごとに AttributeGraph内で行われています どれを見ても ビューの更新が必要である 具体的な理由はわかりません

    SwiftUIは宣言型のため バックトレースからは ビューの更新の 理由を把握できません では SwiftUIビューの更新の原因は どうすればわかるでしょうか まず SwiftUIの仕組みを 理解する必要があります

    簡単なビューの例を通して SwiftUIが データモデルであることを説明します AttributeGraphは ビュー間の依存関係を定義して 必要がない限り ビューの再実行を回避します 今日は細部まで説明しませんが ここでアプリの更新の流れを 理解するうえでの基礎を学べると思います

    ビューはViewプロトコルへの 準拠を宣言します 次に bodyプロパティを実装して 別のView値を返すことで 外観と動作を定義します

    OnOffViewは 本体からTextビューを返し isOn状態変数の 値に応じて変化するラベルを 渡します

    このビューがビュー階層に 最初に追加されるとき SwiftUIは ビュー構造体を格納する親ビューから 属性というオブジェクトを 受け取ります ビュー構造体は頻繁に作り直されますが 属性はIDを維持し ビューの有効期間を通して状態を保ちます そのため親ビューの更新時には この属性の値は変わっても そのIDは変わりません ビューは独自の属性を作成して 状態を格納し 動作を定義するよう 求められます そこでまず isOn状態変数用のストレージと その状態変数の変化をトラッキングする 属性が作成されます 次に ビューは本体を実行するための 新しい属性を作成します 本体はこの両方の属性に依存します ビュー本体の属性は 新しい値の生成を求められるたびに 親ビューから渡された ビューの現在の値を 読み取ります 次に 属性は そのビュー構造体のコピーを 状態変数の現在の値で更新します 次に そのビューの一時的なコピーの 「body」計算プロパティにアクセスし それから返される値を 属性の更新値として保存します ビューの本体がTextビューを返したため SwiftUIはテキストの表示に必要な 属性を設定します

    テキストビューは 環境に依存する属性を作成します これは 前景色やフォントなど 現在のデフォルトのスタイルにアクセスして レンダリングされるテキストの外観を 決定するためです この属性はビュー本体への 依存関係を追加して 返されたText構造体からレンダリングする 文字列にアクセスします 最後に Textはもう1つ属性を作成します スタイルテキストに基づいて レンダリングする内容を説明する属性です

    次に 状態変数を変更すると 何が起こるかについて説明します 状態変数を変更すると SwiftUIは ビューをただちに更新するのではなく 新しいトランザクションを作成します トランザクションは 次のフレームまでに行う必要のある SwiftUIビュー階層に対する 変更を表します

    このトランザクションは状態変数の シグナル属性を期限切れとして マークします 次のフレームに向けた更新の準備ができると SwiftUIは トランザクションを実行し スケジュールされた更新を適用します 属性が期限切れとしてマークされたので

    SwiftUIは この期限切れとなった属性に依存する 一連の属性をたどり それぞれを フラグの設定によって 期限切れとマークします フラグの設定は速やかに実行され 追加の作業はまだ行われません その他のトランザクションの実行が済むと SwiftUIはこのフレームで画面に描画すべき 内容を特定する必要がありますが その情報は期限切れとマークされているため アクセスできません

    SwiftUIは描画内容を決定するために この情報のすべての依存関係を 更新していきます

    更新はStateシグナルなど 期限切れの依存関係が ないものから開始されます これでビュー本体の属性の 更新準備が整いました 再実行され 更新された文字列によって 新しいText構造体の値が生成されます これは既存の Apply styling属性に渡され 描画すべき内容を 特定するために必要な属性が すべて更新されるまで 更新が続行されます これでSwiftUIは 「画面に何を描画すべきか」という 疑問に答えることができます

    「なぜビュー本体が実行されたのか」と 考えるときの真の問題は 「何がビュー本体を期限切れと マークしたのか」です 他のビューなどの依存関係によって ビュー本体がいつ期限切れと マークされるかは 特にそれらのビューが 自身のものであれば制御可能です ただし SwiftUIはビューを表示するために 追加の作業も行います この作業は必須で 通常は避けられませんが いつ行われるのかを 理解しておくことは有益です ビュー更新の原因と結果の 両方に関する情報が明らかになることは 新しいSwiftUI Instrumentの 大きな特長です 原因と結果グラフには そういった原因と結果の関係が すべて記録され このようなグラフで表示されます

    まず 今調査しているビュー本体の 更新を確認します 更新はノードとして表示され ビュー本体の 更新であることを示すアイコンと それに対応するビュータイプを示す タイトルが表示されます

    そのノードに対して 状態の変更を表す ノードから矢印が出ています 矢印に「Update」とあるのは ビューの更新を生じさせたのが 状態の変更だからです また 端に「Creation」という ラベルがあれば それはビューを最初にビュー階層に 表示させたものです

    状態の変更ノードのタイトルは 状態変数の名前と それが関連付けられている ビューのタイプを示しています 状態の変更を選択すると 値が更新された場所の バックトレースが表示されます

    続いて原因と結果グラフの 左側を見ると 状態の変更が ボタンのタップなど ジェスチャーによって 生じたことがわかります

    Landmarksアプリの原因グラフから 何がわかるでしょうか 原因と結果グラフを確認して ビュー本体の余計な更新が 発生した理由を解明しましょう

    これが原因と結果グラフのビューです LandmarkListItemView.bodyの ノードが選択されています グラフの青いノードが表しているのは 私自身のコードの一部か またはアプリの操作中に 私が実行したアクションです グラフでは 原因と結果の連鎖が 左から右に進んでいます

    「Gesture」ノードはお気に入りボタンの タップを表しています

    タップによって お気に入りの ランドマークの配列が更新され

    それにより LandmarkListItemViewの 本体が何度も更新されました 予想よりもはるかに多い回数です

    1つのお気に入りボタンの タップによって 私がタップしたものだけでなく 画面上の多数のアイテムビューが 更新されているようです コードに戻って 何が起こっているのかを 突き止めましょう

    LandmarkListItemViewに戻ります

    modelData.isFavoriteを呼び出し ランドマークを渡すことで ランドマークがお気に入りに なっているかどうかを確認します ModelDataは最上位のモデルオブジェクトで @Observableマクロを使用します それにより プロパティの変更に応じて ビューが更新されます commandキーを押しながらisFavoriteを クリックし この関数に移動します

    ここでfavoritesCollection.landmarks 配列にアクセスし このランドマークがお気に入りか確認します そうすると @Observableが 各アイテムビューと お気に入りの配列全体との間に 依存関係を確立します そのため お気に入りを配列に 追加するたびに 配列が変更されるので 各アイテムビューの本体が実行されます この仕組みを説明します

    こちらは3つのLandmarkListItemViewsです そしてこちらはfavoritesCollectionを持つ ModelDataクラスで これがお気に入りの ランドマークを追跡します 今は ランドマーク2だけが お気に入りです� ModelDataクラスには isFavorite関数があり 各LandmarkListItemViewは この関数を呼び出して アイコンをハイライトするか どうかを決定します

    isFavorite関数は コレクションにランドマークが 含まれているかどうかをチェックし ビューは各自のボタンをレンダリングします 各ビューは 間接的ですが お気に入りの配列に アクセスしたため @Observableマクロは それぞれのビューについて お気に入りの配列全体への 依存関係を作成しました

    では 新しいお気に入りを追加するため 他のビューの お気に入りボタンをタップすると どうなるでしょうか ビューがtoggleFavoriteを呼び出して 新しいランドマークが お気に入りに追加されます すべてのLandmarkListItemViewsには favoritesCollectionへの 依存関係があるため すべてのビューが期限切れとマークされ それらの本体は再実行されます

    私が変更したのは 3のビューだけなので これは最適な動作ではありません 必要なのは ビューのデータ依存関係を より細分化して アプリのデータが 変更されたときに 必要な ビュー本体だけが 更新されるようにすることです

    少し見直してみましょう 各ビューには お気に入りであるかどうかの ステータスを持った ランドマークがあります

    このステータスを追跡するために ビューのObservable ビューモデルを作成します このモデルには お気に入りステータスを 追跡するisFavoriteプロパティがあり 各ビューは 独自のビューモデルを持ちます

    ビューモデルをModelDataクラスに 格納できるようになりました 各ビューは自身のモデルを取得し 必要に応じてお気に入りの状態を 切り替えられます 各ビューは お気に入りの配列全体に 依存するのではなく 各自のランドマークのビューモデルのみに 直接依存します お気に入りを追加してみましょう ボタンをタップすると ToggleFavoriteが 呼び出され ビュー1の モデルが更新されます ビュー1は自身のビューモデルだけに 依存しているため このビューのみ 本体が再実行されます これらの変更がLandmarksにもたらした 結果を見てみましょう

    こちらは新しいビューモデルの実装後に 記録したトレースです サブトラックを 再度クリックし タイムラインの 前と同じ部分を選択します

    詳細ペインで プロセスと

    Landmarksモジュールを展開します

    更新が2つだけになっています 変更したお気に入りは2つなので 適切でしょう 念のためグラフも確認しましょう ビュー名にカーソルを合わせて 矢印をクリックし を選択します

    またグラフが表示されます

    @Observableノードから ビュー本体への矢印が示す更新は 2つのみで 各ボタンに1つずつになっています 各アイテムビューがお気に入りの 配列全体に依存していたのを 密接に結び付いたビューモデルに 置き換えることで ビュー本体の 無駄な更新が大幅に減りました これで アプリはスムーズに 動作するでしょう この例では ビュー本体の更新の原因が 限定的なものだったため グラフは比較的小型でした しかし 原因がより顕著な場合は グラフがもっと大きくなります それが起こり得る状況の1つが ビューが環境から読み取るときです Jed 例を示してもらえませんか 了解です まず 環境の仕組みについて 説明します 環境の値はEnvironmentValues 構造体に格納されます これは値型であり 辞書に似ています これらの各ビューは環境プロパティ ラッパーを使用して環境にアクセスするため EnvironmentValues構造体全体に 依存しています 環境内の値が更新されると 環境への依存関係を持つ各ビューには 本体を実行する必要があると通知されます すると各ビューは 読み取り中の値が 変更されたかどうかを確認します 値が変更された場合は ビュー本体を再実行する必要があります 変更がなかった場合 ビューはすでに最新の状態のため SwiftUIは ビュー本体の実行をスキップできます 原因と結果グラフでこれらの更新が どのようになるか見てみましょう

    環境の更新を表すグラフには 主に2種類のノードがあります External Environmentの更新には SwiftUIの外部から更新される カラースキームなど アプリレベルの更新があります EnvironmentWriterの更新は SwiftUI内で起こる 環境内の値の変更を表します このカテゴリに当てはまるのは .environment修飾子を使用して アプリ内で行う更新です 例えばデバイスがダークモードに 切り替えられて カラースキームの環境値が 更新された場合 これらのビューの 原因と結果グラフは どのようになるでしょうか グラフにはView1の「External Environment」のノードが表示されます カラースキームはシステムレベルの 環境の更新だからです グラフには View1の本体が 実行されたことを示すノードも表示されます View2も環境を読み取るため View2にも原因としてExternal Environmentの更新が示されています ただしView2はカラースキーム値を 読み取らないため 本体は実行されません グラフでは 本体が実行されなかった ビューの更新は 淡色表示のアイコンで表されます この場合 これら2つの外部環境ノードは 同じ更新を表します 同じ更新のいずれかのノードに カーソルを合わせるか クリックすると わかりやすいように どちらも同時に強調表示されます 両方のビューの更新が グラフに表示されるのは 環境の更新の結果として ビュー本体を 実行する必要がない場合でも ビューに関連する値の更新を チェックするために コストがかかるからです 環境から読み取りを行うビューが アプリに多数ある場合 かかる時間はすぐ長くなります そのため 環境には ジオメトリ値やタイマーなど 頻繁に更新される値を 格納しないことが重要です 原因と結果グラフの説明は以上です このグラフはアプリ内のデータの流れを 簡単に視覚化して ビューに無駄な更新を 行わせないようにするために役立ちます このセッションでは SwiftUIアプリで優れたパフォーマンスを 実現するための ベストプラクティスを説明しました ビュー本体を高速に保つことが 重要です それにより SwiftUIが画面にUIを表示する 十分な時間を得られ 遅延をなくせます ビュー本体の不要な更新は 膨れ上がる可能性があるので 必要な場合のみビューが更新されるよう データフローを設計し 頻繁に変更される依存関係には 特に注意しましょう また 開発中の早い段階で頻繁に Instrumentsを使用し アプリのパフォーマンスを 分析するようにしてください 今日はたくさんのことを説明しましたが 最も重要なポイントは1つです SwiftUIの高いパフォーマンスを 確保するため ビュー本体の更新はすばやく 必要な時にのみ行うようにしましょう 開発過程ではSwiftUI Instrumentを使って アプリのパフォーマンスを確認してください

    本日は SwiftUI Instrumentによる アプリのプロファイリングについて 説明しましたが 学ぶべきことは他にもあります ビデオの説明にリンクされている ドキュメントを確認して このInstrumentの他の機能について 学習してください また アプリのパフォーマンスの 分析と向上についての その他の動画や参考資料への リンクも追加しておきました ご視聴ありがとうございました ぜひ 新しいSwiftUI Instrumentを使用して アプリのパフォーマンスを 最大限に高めてください

    • 8:47 - LandmarkListItemView

      import SwiftUI
      import CoreLocation
      
      /// A view that shows a single landmark in a list.
      struct LandmarkListItemView: View {
          @Environment(ModelData.self) private var modelData
      
          let landmark: Landmark
      
          var body: some View {
              Image(landmark.thumbnailImageName)
                  .resizable()
                  .aspectRatio(contentMode: .fill)
                  .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                  .overlay { ... }
                  .clipped()
                  .cornerRadius(Constants.cornerRadius)
                  .overlay(alignment: .bottom) {
                      VStack(spacing: 6) {
                          Text(landmark.name)
                              .font(.title3).fontWeight(.semibold)
                              .multilineTextAlignment(.center)
                              .foregroundColor(.white)
      
                          if let distance {
                              Text(distance)
                                  .font(.callout)
                                  .foregroundStyle(.white.opacity(0.9))
                                  .padding(.bottom)
                          }
                      }
                  }
                  .contextMenu { ... }
          }
      
          private var distance: String? {
              guard let currentLocation = modelData.locationFinder.currentLocation else { return nil }
              let distance = currentLocation.distance(from: landmark.clLocation)
      
              let numberFormatter = NumberFormatter()
              numberFormatter.numberStyle = .decimal
              numberFormatter.maximumFractionDigits = 0
      
              let formatter = MeasurementFormatter()
              formatter.locale = Locale.current
              formatter.unitStyle = .medium
              formatter.unitOptions = .naturalScale
              formatter.numberFormatter = numberFormatter
              return formatter.string(from: Measurement(value: distance, unit: UnitLength.meters))
          }
      }
    • 12:13 - LocationFinder Class with Cached Distance Strings

      import CoreLocation
      
      /// A class the app uses to find the current location.
      @Observable
      class LocationFinder: NSObject {
          var currentLocation: CLLocation?
          private let currentLocationManager: CLLocationManager = CLLocationManager()
      
          private let formatter: MeasurementFormatter
      
          override init() {
              // Format the numeric distance
              let numberFormatter = NumberFormatter()
              numberFormatter.numberStyle = .decimal
              numberFormatter.maximumFractionDigits = 0
      
              // Format the measurement based on the current locale
              let formatter = MeasurementFormatter()
              formatter.locale = Locale.current
              formatter.unitStyle = .medium
              formatter.unitOptions = .naturalScale
              formatter.numberFormatter = numberFormatter
              self.formatter = formatter
      
              super.init()
              
              currentLocationManager.desiredAccuracy = kCLLocationAccuracyKilometer
              currentLocationManager.delegate = self
          }
      
          // MARK: - Landmark Distance
      
          var landmarks: [Landmark] = [] {
              didSet {
                  updateDistances()
              }
          }
      
          private var distanceCache: [Landmark.ID: String] = [:]
      
          private func updateDistances() {
              guard let currentLocation else { return }
      
              // Populate the cache with each formatted distance string
              self.distanceCache = landmarks.reduce(into: [:]) { result, landmark in
                  let distance = self.formatter.string(
                      from: Measurement(
                          value: currentLocation.distance(from: landmark.clLocation),
                          unit: UnitLength.meters
                      )
                  )
                  result[landmark.id] = distance
              }
          }
      
          // Call this function from the view to access the cached value
          func distance(from landmark: Landmark) -> String? {
              distanceCache[landmark.id]
          }
      }
      
      extension LocationFinder: CLLocationManagerDelegate {
          func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
              switch currentLocationManager.authorizationStatus {
              case .authorizedWhenInUse, .authorizedAlways:
                  currentLocationManager.requestLocation()
              case .notDetermined:
                  currentLocationManager.requestWhenInUseAuthorization()
              default:
                  currentLocationManager.stopUpdatingLocation()
              }
          }
          
          func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
              print("Found a location.")
              currentLocation = locations.last
              // Update the distance strings when the location changes
              updateDistances() 
          }
          
          func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
              print("Received an error while trying to find a location: \(error.localizedDescription).")
              currentLocationManager.stopUpdatingLocation()
          }
      }
    • 16:51 - LandmarkListItemView with Favorite Button

      import SwiftUI
      import CoreLocation
      
      /// A view that shows a single landmark in a list.
      struct LandmarkListItemView: View {
          @Environment(ModelData.self) private var modelData
      
          let landmark: Landmark
      
          var body: some View {
              Image(landmark.thumbnailImageName)
                  .resizable()
                  .aspectRatio(contentMode: .fill)
                  .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                  .overlay { ... }
                  .clipped()
                  .cornerRadius(Constants.cornerRadius)
                  .overlay(alignment: .bottom) { ... }
                  .contextMenu { ... }
                  .overlay(alignment: .topTrailing) {
                      let isFavorite = modelData.isFavorite(landmark)
                      Button {
                          modelData.toggleFavorite(landmark)
                      } label: {
                          Label {
                              Text(isFavorite ? "Remove Favorite" : "Add Favorite")
                          } icon: {
                              Image(systemName: "heart")
                                  .symbolVariant(isFavorite ? .fill : .none)
                                  .contentTransition(.symbolEffect)
                                  .font(.title)
                                  .foregroundStyle(.background)
                                  .shadow(color: .primary.opacity(0.25), radius: 2, x: 0, y: 0)
                          }
                      }
                      .labelStyle(.iconOnly)
                      .padding()
                  }
          }
      }
    • 17:20 - ModelData Class

      /// A structure that defines a collection of landmarks.
      @Observable
      class LandmarkCollection: Identifiable {
          // ...
          var landmarks: [Landmark] = []
          // ...
      }
      
      /// A class the app uses to store and manage model data.
      @Observable @MainActor
      class ModelData {
          // ...
          var favoritesCollection: LandmarkCollection!
          // ...
      
          func isFavorite(_ landmark: Landmark) -> Bool {
              var isFavorite: Bool = false
              
              if favoritesCollection.landmarks.firstIndex(of: landmark) != nil {
                  isFavorite = true
              }
              
              return isFavorite
          }
      
          func toggleFavorite(_ landmark: Landmark) {
              if isFavorite(landmark) {
                  removeFavorite(landmark)
              } else {
                  addFavorite(landmark)
              }
          }
      
          func addFavorite(_ landmark: Landmark) {
              favoritesCollection.landmarks.append(landmark)
          }
      
          func removeFavorite(_ landmark: Landmark) {
              if let landmarkIndex = favoritesCollection.landmarks.firstIndex(of: landmark) {
                  favoritesCollection.landmarks.remove(at: landmarkIndex)
              }
          }
          // ...
      }
    • 20:50 - OnOffView

      struct OnOffView: View {
          @State private var isOn = true
          var body: some View {
              Text(isOn ? "On" : "Off")
          }
      }
    • 29:21 - Favorites View Model Class

      @Observable class ViewModel {
          var isFavorite: Bool
          
          init(isFavorite: Bool = false) {
              self.isFavorite = isFavorite
          }
      }
    • 29:21 - ModelData Class with New ViewModel

      @Observable @MainActor
      class ModelData {
          // ...
          var favoritesCollection: LandmarkCollection!
          // ...
      
          @Observable class ViewModel {
              var isFavorite: Bool
              init(isFavorite: Bool = false) {
                  self.isFavorite = isFavorite
              }
          }
      
          // Don't observe this property because we only need to react to changes
          // to each view model individually, rather than the whole dictionary
          @ObservationIgnored private var viewModels: [Landmark.ID: ViewModel] = [:]
      
          private func viewModel(for landmark: Landmark) -> ViewModel {
              // Create a new view model for a landmark on first access
              if viewModels[landmark.id] == nil {
                  viewModels[landmark.id] = ViewModel()
              }
              return viewModels[landmark.id]!
          }
      
          func isFavorite(_ landmark: Landmark) -> Bool {
              // When a SwiftUI view, such as LandmarkListItemView, calls
              // `isFavorite` from its body, accessing `isFavorite` on the 
              // view model here establishes a direct dependency between
              // the view and the view model
              viewModel(for: landmark).isFavorite
          }
      
          func toggleFavorite(_ landmark: Landmark) {
              if isFavorite(landmark) {
                  removeFavorite(landmark)
              } else {
                  addFavorite(landmark)
              }
          }
      
          func addFavorite(_ landmark: Landmark) {
              favoritesCollection.landmarks.append(landmark)
              viewModel(for: landmark).isFavorite = true
          }
      
          func removeFavorite(_ landmark: Landmark) {
              if let landmarkIndex = favoritesCollection.landmarks.firstIndex(of: landmark) {
                  favoritesCollection.landmarks.remove(at: landmarkIndex)
              }
              viewModel(for: landmark).isFavorite = false
          }
          // ...
      }
    • 31:34 - Cause and effect: EnvironmentValues

      struct View1: View {
          @Environment(\.colorScheme)
          private var colorScheme
      
          var body: some View {
              Text(colorScheme == .dark
                      ? "Dark Mode"
                      : "Light Mode")
          }
      }
      
      struct View2: View {
          @Environment(\.counter) private var counter
      
          var body: some View {
              Text("\(counter)")
          }
      }

Developer Footer

  • ビデオ
  • WWDC25
  • Instrumentsによる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.
    利用規約 プライバシーポリシー 契約とガイドライン