@self changed
地獄
描画が更新されない悪夢と無限地獄編
🧱 はじめに
画面を開いたら、何も出ない。
でも body
に仕込んだ print("🔄 body evaluated")
だけは、延々とターミナルに流れ続ける。
SwiftUIの画面に何が起きているのかもわからないまま、
コンソールだけが「生きている」と訴えかけてくる――。
🔄 RoleDetailView body evaluated
RoleDetailView: `@self` changed.
🔄 RoleDetailView body evaluated
RoleDetailView: `@self` changed.
...
これは筆者が遭遇した「描画が更新されないSwiftUI無限地獄」の記録です。
SwiftUI × SwiftData × NavigationStack のトリプルコンボで発生したこの現象、
**原因は意外と“たった1行のコード”**だったりします。
✅ 本記事で扱うこと
- SwiftUIで画面が表示されず無限ループが起きるケース
-
@self changed.
の意味と原因 - 爆発しない構造の作り方
- SwiftDataを使った編集画面を安全に作る方法
🧟♂️ 第1章:悪夢の始まり
症状 〜画面が表示されない恐怖と、終わらない body
〜
ある編集画面を作っていたときのこと。
何の変哲もない NavigationDestination
を使い、
新しいデータを登録する画面に遷移しようとした――
それだけのはずだった。
ところが、画面は真っ白。何も表示されない。
「おかしいな…」と body
にログを仕込んでみると――
🔄 RoleDetailView body evaluated
RoleDetailView: `@self` changed.
(繰り返し)
Viewの構造が「前回と違う」とSwiftUIが判定し続け、
⚠️ 今回の無限ループは、いわゆる「循環参照的構造」が原因となっており、SwiftUIが同一Viewとみなせないことで描画が止まらなくなります。
再評価 → 再構築 → 再評価… のループに突入。
画面は描画されず、メモリだけが静かに食い尽くされていく…。
🕵️♂️ 第2章:原因を探る
@self changed
を引き起こした意外な犯人
Viewの構造が毎回変わる、つまり何かが「毎回違う値」になっている。
調べていくと、こんなコードに辿り着いた。
var hoge = Role("")
この Role
は SwiftData のマネージドオブジェクト。
つまり参照型であり、Viewのたびに 毎回新しいインスタンスが生成される。
→ SwiftUIは「このView、さっきのと違うやん」
→ @self changed.
→ body
再評価
→ また違う
→ 地獄再開 🔁
🔥 第3章:暴走するのはいつ?
条件が揃ったときだけ発動する @self
無限ループ
おかしいのは、常に暴走するわけではなかったこと。
- テストメニューから
RoleDetailView
に遷移 → 正常に表示 - 一覧画面から
NavigationDestination
経由で遷移 → 無限ループ発生
調べた結果、次の条件が揃ったときだけ爆発すると判明。
💥 地獄コンボ条件
条件 | 内容 |
---|---|
1 | SwiftDataモデルを View 内で Role() などで new |
2 |
NavigationDestination のクロージャ内で View を直接生成 |
→ SwiftUIは**「毎回別のView」と誤認**して再描画を繰り返す。
🚪 第4章:無限地獄からの脱出
SwiftUI × SwiftData で安全に詳細画面を開く方法
✅ 安定する構成(地雷回避3ステップ)
1. モデルは @State var item: Model?
で親に保持する
@State private var newRole: Role? = nil
2. .navigationDestination(item:)
を使う
.navigationDestination(item: $newRole) { role in
RoleDetailView(role: role, isNew: true)
}
3. 編集Viewでは @Bindable
で受け取る
@Bindable var role: Role
この構成なら、View構造は変わらず、SwiftUIも混乱しない。
🧭 第5章:この地雷を避けろ
SwiftData × NavigationStack で暴れないためのルール
❌ やってはいけない
// View内でマネージドモデルを new
var role = Role("") // ❌ NG
// NavigationDestination の中で new
.navigationDestination(isPresented: $isNew) {
RoleDetailView(role: Role(""), isNew: true) // ❌ NG
}
// SwiftData モデルを State に持つ
@State var role: Role // ❌ NG
✅ 正しいやり方まとめ
- モデルは外部で new して
@State
で保持 -
.navigationDestination(item:)
を使って同一性を担保 - Viewは
@Bindable
で受け取って編集
🧠 こう覚えよう:
Viewでnewするな!ViewにStateするな!Navigationで混ぜるな!
🌟 おわりに
SwiftUIは賢いけれど、ちょっとした違いにも敏感すぎる。
その結果として生まれる @self changed.
無限ループ。
小さな構造の違いが、大きな暴走を引き起こす。
この記事が、同じような地獄に片足突っ込みかけてる誰かの助けになれば幸いです。