0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NavigationDestination地獄:@self changedが止まらない (SwiftUI × SwiftDataが暴走した理由とその回避策)

Last updated at Posted at 2025-04-05

A_digital_illustration_features_a_SwiftUI_icon_at_.png

@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の構造が毎回変わる、つまり何かが「毎回違う値」になっている

調べていくと、こんなコードに辿り着いた。

swift
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? で親に保持する

swift
@State private var newRole: Role? = nil

2. .navigationDestination(item:) を使う

swift
.navigationDestination(item: $newRole) { role in
    RoleDetailView(role: role, isNew: true)
}

3. 編集Viewでは @Bindable で受け取る

swift
@Bindable var role: Role

この構成なら、View構造は変わらず、SwiftUIも混乱しない。


🧭 第5章:この地雷を避けろ

SwiftData × NavigationStack で暴れないためのルール

❌ やってはいけない

swift
// View内でマネージドモデルを new
var role = Role("") // ❌ NG
swift
// NavigationDestination の中で new
.navigationDestination(isPresented: $isNew) {
    RoleDetailView(role: Role(""), isNew: true) // ❌ NG
}
swift
// SwiftData モデルを State に持つ
@State var role: Role // ❌ NG

✅ 正しいやり方まとめ

  • モデルは外部で new して @State で保持
  • .navigationDestination(item:) を使って同一性を担保
  • Viewは @Bindable で受け取って編集

🧠 こう覚えよう:

Viewでnewするな!ViewにStateするな!Navigationで混ぜるな!


🌟 おわりに

SwiftUIは賢いけれど、ちょっとした違いにも敏感すぎる。
その結果として生まれる @self changed. 無限ループ。

小さな構造の違いが、大きな暴走を引き起こす。

この記事が、同じような地獄に片足突っ込みかけてる誰かの助けになれば幸いです。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?