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

![A_digital_illustration_features_a_SwiftUI_icon_at_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2301761/06141203-b6b4-4b89-b8be-d98bcd6a3ceb.png)
# `@self changed`地獄

### 描画が更新されない悪夢と無限地獄編

---

## 🧱 はじめに

画面を開いたら、何も出ない。\
でも `body` に仕込んだ `print("🔄 body evaluated")` だけは、延々とターミナルに流れ続ける。

SwiftUIの画面に何が起きているのかもわからないまま、\
**コンソールだけが「生きている」と訴えかけてくる――。**

```log
🔄 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` にログを仕込んでみると――

```log
🔄 RoleDetailView body evaluated
RoleDetailView: `@self` changed.
（繰り返し）
```

Viewの構造が「前回と違う」とSwiftUIが判定し続け、
⚠️ 今回の無限ループは、いわゆる「循環参照的構造」が原因となっており、SwiftUIが同一Viewとみなせないことで描画が止まらなくなります。

**再評価 → 再構築 → 再評価…** のループに突入。\
画面は描画されず、**メモリだけが静かに食い尽くされていく…**。

---

## 🕵️‍♂️ 第2章：原因を探る

### `@self changed` を引き起こした意外な犯人

Viewの構造が毎回変わる、つまり**何かが「毎回違う値」になっている**。

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

```swift: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:swift
@State private var newRole: Role? = nil
```

#### 2. `.navigationDestination(item:)` を使う

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

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

```swift:swift
@Bindable var role: Role
```

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

---

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

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

### ❌ やってはいけない

```swift:swift
// View内でマネージドモデルを new
var role = Role("") // ❌ NG
```

```swift:swift
// NavigationDestination の中で new
.navigationDestination(isPresented: $isNew) {
    RoleDetailView(role: Role(""), isNew: true) // ❌ NG
}
```

```swift:swift
// SwiftData モデルを State に持つ
@State var role: Role // ❌ NG
```

### ✅ 正しいやり方まとめ

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

### 🧠 こう覚えよう：

> Viewでnewするな！ViewにStateするな！Navigationで混ぜるな！

---

## 🌟 おわりに

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

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

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