swiftui-debugging について
swiftui-debuggingは、SwiftUI開発者向けのパフォーマンス診断ガイドです。ビューの更新、アイデンティティ、遅延読み込みを扱い、計測・特定・最適化・再計測の手順とInstrumentsの活用法を示します。
主な機能
- 01ビューの再評価を診断
- 02ビューのアイデンティティと差分を確認
- 03リストの遅延読み込みを分析
- 04Instrumentsで処理時間を特定
- 05依存する監視範囲を限定
活用シーン
- 01スクロールのカクつきを調査
- 02ビューが繰り返し更新される原因を特定
- 03リストの読み込みと描画負荷を軽減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rshankras/ claude-code-apple-skills --skill swiftui-debuggingインストール後、Claude Code で/swiftui-debuggingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
body-reevaluation.mdcommon-pitfalls.mdlazy-loading.mdview-identity.md詳細
SwiftUI のパフォーマンスデバッグ
SwiftUI のパフォーマンス問題を診断し、修正するための体系的なガイドです。不要なビューの再評価、アイデンティティの問題、コストの高い body の計算、遅延読み込みの誤りを扱います。
このスキルが有効になる場面
次のような場合にこのスキルを使用してください。
- SwiftUI のビューが遅い、またはカクつくと報告された場合
- ビューの再レンダリングや
bodyの再評価が過剰に発生している場合 Self._printChanges()やビューのデバッグについて質問された場合- リストやグリッドのスクロール性能に問題がある場合
- 何も変わっていないのにビューが更新され続ける理由を質問された場合
@ObservableとObservableObjectのパフォーマンスの違いに言及された場合- SwiftUI のビューのアイデンティティや差分計算について知りたい場合
AnyViewを使用していて、パフォーマンスへの影響を質問された場合- SwiftUI のレンダリングが原因でハングやカクつきが発生している場合
パフォーマンス改善のループ
調査はすべて同じループに従います。症状 -> 測定 -> 特定 -> 最適化 -> 再測定。最後のステップは省略されがちですが、再測定していない「最適化」は単なる推測です。SwiftUI では推測が外れることも珍しくありません(修正によって別の場所にコストが移る場合があります)。そのため、このループは欠かせません。差分だけを根拠にパフォーマンスの問題を解決済みにしないでください。
判断ツリー
What SwiftUI performance problem are you seeing?
|
+- Views re-render when they should not
| +- Read body-reevaluation.md
| +- Self._printChanges() to identify which property changed
| +- @Observable vs ObservableObject observation differences
| +- Splitting views to narrow observation scope
|
+- Scrolling is slow / choppy (lists, grids)
| +- Read lazy-loading.md
| +- VStack vs LazyVStack, ForEach without lazy container
| +- List prefetching, grid cell reuse
|
+- Views lose state unexpectedly / animate when they should not
| +- Read view-identity.md
| +- Structural vs explicit identity
| +- .id() misuse, conditional view branching
|
+- Known pitfall (AnyView, DateFormatter in body, etc.)
| +- Read common-pitfalls.md
| +- AnyView type erasure, object creation in body
| +- Over-observation, expensive computations
|
+- General "my SwiftUI app is slow" (unknown cause)
| +- Start with body-reevaluation.md, then common-pitfalls.md
| +- Use Instruments SwiftUI template (see Debugging Tools below)
API の利用可能バージョン
| API/手法 | 最小バージョン | リファレンス |
|---|---|---|
Self._printChanges() | iOS 15 | body-reevaluation.md |
@Observable | iOS 17 / macOS 14 | body-reevaluation.md |
@ObservableObject | iOS 13 | body-reevaluation.md |
LazyVStack / LazyHStack | iOS 14 | lazy-loading.md |
LazyVGrid / LazyHGrid | iOS 14 | lazy-loading.md |
.id() 修飾子 | iOS 13 | view-identity.md |
| Instruments SwiftUI テンプレート | Xcode 14+ | SKILL.md |
| 再設計された SwiftUI インストゥルメント | Xcode 26 / Instruments 26 | SKILL.md |
os_signpost | iOS 12 | SKILL.md |
よくある間違いトップ 5 -- クイックリファレンス
| # | 間違い | 修正方法 | 詳細 |
|---|---|---|---|
| 1 | LazyVStack などの遅延コンテナーを使わずに、大きな ForEach を VStack や ScrollView 内に配置する | LazyVStack でラップします。通常の VStack はすべてのビューを最初に作成します | lazy-loading.md |
| 2 | 型を消去するために AnyView を使う | @ViewBuilder、Group、または具体的なジェネリック型を使います。AnyView は差分計算を妨げます | common-pitfalls.md |
| 3 | body 内でオブジェクト(DateFormatter()、NumberFormatter())を作成する | 共有インスタンスには static let を、変更可能なオブジェクトには @State を使います | common-pitfalls.md |
| 4 | 必要なプロパティが 1 つだけなのに、モデル全体を監視する | @Observable オブジェクトをより小さく分割するか、サブビューを切り出します | body-reevaluation.md |
| 5 | 不安定な .id() の値によって、レンダリングのたびにビュー全体が再作成される | 安定した識別子(データベース ID、UUID)を使い、配列のインデックスやランダムな値は使いません | view-identity.md |
List、Table、ForEach:アイデンティティがコストモデルを決める
List と Table は読み込み時にすべての識別子を先行して収集します。行のビューは遅延読み込みされますが、識別子の収集は行われます。低コストで事前計算済みの ID なら高速に読み込めますが、計算やハッシュ化にコストがかかる id: キーパスを使うと、何かがレンダリングされる前にすべての要素について処理が実行されます。
行数の計算式: 行数 = 要素数 x 要素あたりのビュー数です。要素あたりのビュー数は、フレームワークがクロージャを実行せずに読み取れる定数でなければなりません。
- ❌
ForEach内ではifによるフィルタリングもAnyViewも使わないでください。どちらも要素あたりのビュー数を定数でなくし、行数を数えるためだけに SwiftUI がすべてのクロージャを実行することになります(Listの定数行数の最適化も妨げます)。 - ✅ ビューではなくデータをフィルタリングし、フィルタリング済みのコレクションをモデルにキャッシュしてください。
body内にitems.filter { ... }をインラインで記述すると、bodyが評価されるたびに全要素を走査し直します。 - ✅
Tableでは、簡潔なForEach(collection)イニシャライザー(id:なし、行ごとのクロージャ操作なし)を優先してください。これにより、行数を静的な定数として維持できます。
更新が遅くなる一般的な原因
プロファイルで本文や更新の表示が遅い場合、原因はほぼ次のいずれかです。
- 動的プロパティの初期化にコストがかかる -- 例:初期化子で I/O を行う
@StateObject/@Stateオブジェクト body内の処理 -- 文字列の補間やフォーマット、並べ替え、フィルタリングbody内のヒープ割り当て -- フォーマッタ、述語、中間配列body内のバンドル/リソース検索 --Bundle.mainの検索、画像の同期デコード
読み込みやコストのかかる派生処理は .task(またはモデル)に移してから、もう一度測定してください。
依存関係の範囲は絞ってください。ただし、必要以上に細分化しないでください。 子ビューにはモデルオブジェクト全体ではなく、描画する Image を渡せば、無関係なモデルの変更で子ビューが無効化されなくなります。とはいえ、細分化しすぎないようにしてください。巨大な構造体を、単一プロパティのパラメーターを何十個も持つ形に分割すると、得られる効果よりも配線のコストのほうが大きくなります。@Observable は、body が実際に読み取るプロパティだけに無効化の範囲を絞ってくれるので、まずはこれを活用してください。
デバッグツール
Self._printChanges()
ビューの body に追加すると、再評価の原因を確認できます。
var body: some View {
let _ = Self._printChanges()
// ... view content
}
出力の読み方:ViewName: @self, @identity, _propertyName changed.。簡単に言うと、@self はビューの値が変わったこと(親がビューを再構築したこと)を示し、名前付きプロパティはその特定の依存関係が変わったことを示します。詳しい解釈方法は body-reevaluation.md を参照してください。
コードを編集せずに LLDB から呼び出すこともできます。ビューのコンテキストで一時停止し、次を実行してください。
(lldb) expression Self._printChanges()
リリース前に削除してください。実行時に実際のコストが発生します。
Instruments の SwiftUI テンプレート
- Xcode > Product > Profile(Cmd+I)
- SwiftUI テンプレートを選択します(View Body、View Properties、Core Animation Commits を含みます)
- 記録を開始し、遅い操作を再現してから停止します
- View Body レーンに、
bodyが評価されたビューとその回数が表示されます - View Properties レーンに、変更されたプロパティが表示されます
再設計された SwiftUI 計測器(Instruments 26 -- WWDC25 306)
Xcode 26 と、プロファイル対象のデバイス上の最新 OS リリースが必要です(トレース記録のサポートは OS 側にあります)。Cmd+I を押すと Release ビルドが作成され、Instruments が開きます。SwiftUI テンプレートには、SwiftUI 計測器、Time Profiler、Hangs + Hitches 計測器が含まれています。「どの body がフレームの予算を超過させているか」を調べるときは、ここから始めてください。
body の時間が重要な理由: 各フレームで、アプリはイベントを処理し、変更されたビューの body を実行し、フレームの期限までに処理を終える必要があります。body が期限を超えると、処理全体が遅れ、前のフレームが画面に長く表示されたままになります。これがヒッチです。期限超過につながるパターンは 2 つあります。(1)1 回の body 更新に時間がかかる、(2)個々の処理は速いものの、1 フレーム内で不要な更新が多数発生する(WWDC25 306)。
トップレベルのレーン:
| レーン | 意味 |
|---|---|
| Update Groups | SwiftUI が何らかの処理を行っているタイミングを示します。このレーンが空の間に CPU 使用率が急上昇する場合、問題はSwiftUI の外部にあります。一般的なプロファイリングのスキルに切り替えてください |
| Long View Body Updates | 時間がかかりすぎている body プロパティ |
| Long Representable Updates | 時間がかかりすぎている UIView/UIViewController/NSView の representable 更新 |
| Other Long Updates | その他すべての、時間がかかりすぎている SwiftUI の処理 |
色分け:長時間の更新は、ヒッチやハングの原因となる可能性に応じてオレンジまたは赤で表示されます。まず赤を調べてください。通常の更新は灰色です。トレースの開始直後にある長時間の更新は、起動時の初期階層の構築です。これは想定どおりで、ヒッチの原因にはなりません。調査対象にしないでください(WWDC25 306)。
長時間の body のワークフロー:
- SwiftUI トラックを展開し、View Body Updates サブトラックを選択します。詳細ペインに、実行されたすべての body の概要が表示されます(プロセス -> モジュール -> ビュータイプ)
- 概要のドロップダウンを Long View Body Updates に切り替え、回数とともに問題のある処理に絞り込みます
- ビュー名にカーソルを合わせ、矢印 -> Show Updates -> いずれかを右クリック -> Set Inspection Range and Zoom の順に選択します
- Time Profiler トラックを選択し、その body の実行中に CPU が行った処理を確認します(Option キーを押しながらクリックするとメインスレッドのスタックが展開されます。Cmd+F で body のフレームを検索できます)
- 修正して再記録し、Long View Body Updates の概要からビューが消えたことを確認します
このセッションのデモで見つかった典型的な問題:body で読み取る計算プロパティが、表示中の各行で body が実行されるたびに NumberFormatter と MeasurementFormatter を作成し、文字列をフォーマットしていました。修正方法:フォーマッターを一度だけ作成し(モデルまたはマネージャーの保存プロパティとして保持)、文字列をID キーのキャッシュに事前計算しておき、body では辞書を検索するだけにします(WWDC25 306)。一般的なルールについては common-pitfalls.md を参照してください。
原因と結果のグラフ:なぜ body が実行されたのか?(WWDC25 306)
バックトレースを使えば命令型の UIKit 更新の原因はわかりますが、SwiftUI ではそうはいきません。SwiftUI のバックトレースは AttributeGraph のフレームが再帰的に並ぶだけで、あなたのビューが更新された理由は示されません。つまり、「なぜ body が実行されたのか?」は、実際には「何が body を古い状態としてマークしたのか?」という問いです。これに答えるのが、インストゥルメントの原因と結果のグラフです(ビュー名にカーソルを合わせて矢印を選択 ->「原因と結果のグラフを表示」)。
- ノードは更新を表し、種類ごとにアイコンがあります(ビューの body、状態の変更、ジェスチャー、
@Observableの変更、環境)。青いノード = あなたのコード/操作。グラフは左(原因)から右(結果)へ読みます - エッジには**「update」(再実行を引き起こした)または「Creation」**(ビューを初めて表示した)というラベルが付いています
- 薄く表示されたアイコンは、ビューに通知が届いて確認されたものの、body は実行されなかったことを示します
- 状態変更ノードを選択すると、値が設定された場所のバックトレースが表示されます
このグラフで明らかになったデモのバグは、各行の body が isFavorite(landmark) を呼び出し、@Observable モデルの共有 landmarks 配列を読み取っていたことです。そのため、すべての行が配列全体に依存しており、タップが 1 回あるだけで、表示中のすべての行の body が再実行されていました。
// ❌ Every row reads the shared array -> @Observable makes every row depend on it
func isFavorite(_ landmark: Landmark) -> Bool {
favoritesCollection.landmarks.contains(landmark) // whole-array dependency
}
// ✅ Per-item @Observable view model -> each row depends only on its own flag
@Observable class ViewModel { var isFavorite: Bool = false }
@ObservationIgnored private var viewModels: [Landmark.ID: ViewModel] = [:]
// ^ @ObservationIgnored: don't observe the dictionary itself, only each model
func isFavorite(_ landmark: Landmark) -> Bool {
viewModel(for: landmark).isFavorite // body's read = narrow dependency
}
func addFavorite(_ landmark: Landmark) {
favoritesCollection.landmarks.append(landmark)
viewModel(for: landmark).isFavorite = true
}
トレースで確認した結果:タップ 2 回 = body の更新が正確に 2 回。原則:データの依存関係は、それを描画する UI と同じ粒度にします(WWDC25 306)。
環境に関する原則: EnvironmentValues は 1 つの値型構造体であり、@Environment を使うすべてのビューは構造体全体に依存します。環境に変更があると、そのようなビューすべてに通知が届きます。各ビューは自身のキーの値を比較し、変更されていれば body が再実行されますが、比較自体は値を読み取るすべてのビューで時間がかかります。頻繁に変化する値(ジオメトリ、タイマー)は環境に保存しないでください(WWDC25 306)。グラフでは、SwiftUI の外部で変更された外部環境ノード(例:カラースキーム)と、.environment(...) を通じて変更されたEnvironmentWriterノードが区別されます。
カスタム計測のための os_signpost
import os
private let perfLog = OSLog(subsystem: "com.app.perf", category: "SwiftUI")
var body: some View {
let _ = os_signpost(.event, log: perfLog, name: "MyView.body")
// ... view content
}
Instruments の os_signpost インストゥルメントで表示し、1 秒あたりの body 評価回数を数えます。
レビューチェックリスト
ビューのアイデンティティ
- 不安定な
.id()の値(ランダム値、Date()、変更可能な配列のインデックス)を使っていない - 条件分岐(
if/else)によって不要なビューの破棄が発生していない -
ForEachでモデル由来の安定した一意の識別子を使っている
Body の再評価
- ビューが実際に使うプロパティのみを監視している
-
ObservableObjectより@Observableクラスを優先している(iOS 17 以降) - body の再評価を引き起こす不要な
@Stateの変更がない - 大きなビューを小さなサブビューに分割し、監視範囲を狭めている
遅延読み込み
- 大きなコレクションでは
VStack/HStackではなくLazyVStack/LazyHStackを使っている - 50 個以上の項目には
Listまたは遅延スタックを使っている - 遅延コンテナ内の子要素に
.frame(maxHeight: .infinity)を指定していない(遅延性が損なわれるため)
よくある落とし穴
-
AnyViewによる型消去を使っていない(@ViewBuilderまたはGroupを使用) -
body内でオブジェクトを割り当てていない(DateFormatter、NSPredicate、ビューモデル) - 時間のかかる計算を
task { }またはTask.detachedでバックグラウンドに移している - 画像には
AsyncImageまたは適切なサイズ指定をした.resizable()を使い、body 内で生のUIImageをデコードしていない
参考ファイル
| ファイル | 内容 |
|---|---|
| view-identity.md | 構造的アイデンティティと明示的アイデンティティ、.id() の使い方、条件分岐 |
| body-reevaluation.md | body の実行を引き起こす要因、_printChanges()、@Observable と ObservableObject の比較 |
| lazy-loading.md | 遅延コンテナと即時コンテナ、List、ForEach、グリッドのパフォーマンス |
| common-pitfalls.md | AnyView、body 内でのオブジェクト生成、過剰な監視、時間のかかる計算 |
| ../profiling/SKILL.md | Instruments による一般的なプロファイリング(Time Profiler、Memory、Energy) |
| ../../swiftui/data-flow/SKILL.md | 上記すべての背景にあるアイデンティティ/ライフタイムのメンタルモデル。修正が当てずっぽうに感じられるときは、こちらを読んでください |