画面遷移とViewの再描写

NavigationStack で画面遷移する簡単なコード

ボタンが押されたら遷移する例。
NavigationStack(path: $path)でpath変数を監視していて、
enumで列挙したプロパティが来たら遷移する

👍 画面に応じてパラメータも持たせられる

例えば「ID を持つ詳細画面」が必要なら:

遷移:

受け取り:

柔軟で超使いやすいです。

🎯 まとめ

MyPage は—

  • enum
  • Hashable
  • case に遷移先を列挙

という形で宣言します。

これが SwiftUI の NavigationStack の“正しい使い方”です。

✅ .sheet で画面遷移した場合

遷移元の View(親 View)は “生きています”!

  • .sheet は モーダル表示(上に被せるだけ)
  • 親 View は 破棄されません
  • onAppear も再度呼ばれません
  • @State も @StateObject もそのまま保持される

つまり、
・Sheet が上に乗るだけ
・元の画面は裏でそのまま動き続ける

🧪 動作例(簡単なイメージ)

この時:

  • Sheet を開いても count は維持される
  • Sheet を閉じても count はそのまま

原因:ParentView が破棄されていないから。

遷移方法親Viewの状態再描画生存状態
.sheet生きてるsheetが出ても親はそのまま生きてる
.fullScreenCover生きてる同上生きてる
NavigationStack push生きてる表示外で省エネされることはある生きてる
NavigationStack pop→ 破棄される戻る → 再生成破棄されて新しいのが作られる

🟣 .sheet の重要な注意点

1. .sheet 内側の View は毎回再生成される(ほぼ確実)

2. 逆に親側(元の画面)は破棄されない

→ 親の @State や @StateObject は保持され続ける。

🧩 どういうときに問題になる?

例えば Sheet から戻ったときに

  • 親画面の初期化が走らない
  • 親の ViewModel がそのまま残る
  • 「画面を更新したいのに更新されない」

こういう現象が起きやすいです。

起きていた問題

やはりですか。 まず、親ViewはObservableObjectで保持しているフラグ変数を元に if で動的に作成されるViewがあります。 sheetの子の子ViewのトグルViewでそのObservableObjectのフラグ変数をトグルしたら強制的に親Viewに移行してしまいました。 これは親Viewが再描写されたっぽいですね。

🧠 SwiftUI の厳密な挙動

1. ObservableObject が変更されると

  • それを監視しているすべての View が “body の再評価” を行う
    → 親Viewの body も間違いなく再評価される

2. if flag { ... } else { ... }

のような条件分岐で View を切り替えている場合

  • flag の変更により、全く別の View が作り直される
  • SwiftUI は「前の View を消して、新しい View を作り直す」
    → ナビゲーション状態・sheet 状態が失われることがある

つまり、Sheet を開いている状態でも、親Viewの body が再構築されると、sheet の親が変わり sheet が閉じるという挙動が起きます。

これは SwiftUI の仕様であり、バグではありません。

💥 なぜ「sheet が閉じたように見える」のか?

sheet は 「今表示している View に紐づくモディファイア」 なので、その View が再描画で入れ替わると:

  • sheet の紐づけ先が変わる
  • SwiftUI は「前の View に付いていた sheet を停止」する

結果として、

👉 flag を変更 → 親Viewの identity が変わる → sheet が閉じられる

という流れが起きます。

🔧 解決策(場面ごとに選べる)

❗ 解決策①

🔹 親Viewで条件分岐させる代わりに

子Viewを .sheet や .fullScreenCover を使って出す

つまり「View を if で切り替えない」。
親Viewの identity を変えないようにする。

❗ 解決策②

🔹 ObservableObject の状態変更で画面構造を変えない

(flagでViewを変えないようにする)

例:

のように「View を変えずに中身だけ変える」ことで、
View の identity を安定させる。

❗ 解決策③

🔹 NavigationStack を使い、push/pop で構造を安定させる

親画面の body 再描画で identity が変わる問題が減る。

❗ 解決策④

🔹 sheet は “親View の一番外側” につける

(内部の条件分岐の外に出す)

これも identity を安定させる効果が高い。

❗ 解決策⑤

🔹 ObservableObject を親Viewの外で保持(@StateObject の位置を変える)

こうすると ParentView が再構築されても ViewModel が保たれるため、
画面構造がある程度安定しやすい。

今回の自分の対応。
そもそも設定を行うViewのフラグ変数だったため新たにそのViewの管理クラスを作り、そこにフラグ変数を持たせた。(アプリの機能追加で起きた問題だったが、規模が小さいアプリだからといって1つのObservableObjectクラスに共通変数を全部管理させていたのが元凶)

✔️ NavigationStack で画面遷移した場合

基本ルール:

ObservableObject(@StateObject / @EnvironmentObject)は画面遷移後も状態を保持します。

つまり:

  • NavigationStack の push(画面遷移)
  • pop(戻る)
  • 深い階層への遷移

いずれでも ObservableObject のインスタンスは再生成されません。
→ 状態は維持されます。

✔️ ただし例外ポイントがある(ここが重要)

① 親Viewで @ObservedObject を使っている場合

もしこんな構造になっていると…

これは 遷移のたびに再生成される可能性がある ので NG。

保持したいなら必ず:

@StateObject を使う

もしくは上位の Scene/RootView で作って .environmentObject で渡す。

② NavigationStack 自体を再生成した場合

もし NavigationStack を含む親 View が再描画で作り直されると、その中で作られている状態は初期化されます。

例:

こういうパターンだと sv.showMain の変更で NavigationStack が丸ごと作り直され、
その内部の状態も消える可能性があります。

③ NavigationStack の “path” を @StateObject でなく @State に持っている場合

@State var path: [Screen] = [] は、親Viewが再描画されると内容リセットの可能性があります。

安定させたいなら:

  • path は上位で @StateObject / EnvironmentObject に置く
  • または path を外部に逃がして View に埋め込まない

などの工夫が必要。

✔️ まとめ

状態保持?状態が消える条件
✓ NavigationStack での push/pop は ObservableObject を維持特に無い
⚠︎ @ObservedObject を使うと再生成されるView の再構築時
⚠︎ NavigationStack を条件分岐の中に置くと破棄される分岐条件が変わった時
⚠︎ path がローカル状態だとリセットされやすい親View再描画

✔️ よくある安全パターン

  1. SharedVariables を @StateObject で RootView に置く
  2. NavigationStack は RootView 直下に固定(条件分岐で消さない)
  3. 子画面にも environmentObject で渡す

例:

こうすると 状態がほぼ永続的に安定 します。

画面遷移なしでも View を再構築(= onAppear を再発火)

🔥 最も簡単な方法:View の “identity” を変える

SwiftUI では id が変わると View は完全に別物扱いになる ため、再生成されて onAppear が再度発火します。

この refreshID = UUID() を実行すると:

  • View は完全に再生成される
  • onAppear がもう一度発火する
  • アニメーションやレイアウトも初期化される

🟦 他の方法(状況に応じて使える)

① 親 View で条件分岐して再描画させる

show.toggle() により ChildView が一瞬消えて再度生成 → onAppear 発火。

② NavigationStack で path を操作する(疑似リセット)

もし NavigationStack を使っているなら:

これでスタックのトップ画面が再度構築される。

🟣 「onAppear を再発火させるべきか?」も注意ポイント

SwiftUI的には以下がベストプラクティスです👇

  • 副作用を onAppear に詰め込みすぎない
    (本来は View の「初回表示」だけでやるべき処理)
  • リロードは ViewModel に責務を寄せるほうが安全
    → View の lifecycle に依存しないためバグが減る

とはいえ、
「画面遷移なしで View をリセットしたい」
というケースは多いので .id() を使う方法はとても有効です。

✅ 遷移「する時」に処理を実行する方法

① navigationDestination で遷移前に処理を走らせる(オススメ)

✔ 遷移する瞬間に任意の処理が確実に実行
✔ NavigationStack と相性が最高
✔ 遷移ボタン内に書くので分かりやすい

② カスタム init() を使って遷移時に処理する

遷移先の View がロードされた瞬間に処理を走らせたい場合。

✔ 遷移先が生成された瞬間に一度だけ実行
✔ onAppear より確実(onAppear は再描画だけで呼ばれる)

③ 遷移先の onAppear を使う

よく使われるパターン。

✅ 遷移「から戻る時」に処理を実行する方法

NavigationStack の戻り検知はいくつかやり方があります。

① onDisappear を使う(直感的)

✔ NavigationStack の戻るボタンでも呼ばれる
✔ sheet と違って “ほぼ確実に呼ばれる”

② NavigationStack の path を監視する(最も SwiftUI 的)

パスに要素が追加されたり削除されたりするのを見張る。

✔ 大規模アプリに向く
✔ 画面戻りだけ確実に検知できる

③ 遷移先 View に Binding を渡して戻り処理を起こす

遷移先で:

✔ “戻るとき専用の処理” を View に依存せず書ける
✔ ロジックの依存を減らせる

🎯 まとめ(最もおすすめ構成)

タイミングベストアプローチ
遷移する時の処理遷移ボタン内で実行 or init()
遷移先が表示された時の処理onAppear
戻ってきた時の処理onDisappear or path監視

SwiftUI では「戻り時の処理」だけ少しクセがありますが、
上の方法なら確実に処理できます。

コメント

タイトルとURLをコピーしました