NavigationStack で画面遷移する簡単なコード
ボタンが押されたら遷移する例。
NavigationStack(path: $path)でpath変数を監視していて、
enumで列挙したプロパティが来たら遷移する
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
enum MyPage: Hashable { case next } struct ContentView: View { @State private var path: [MyPage] = [] var body: some View { NavigationStack(path: $path) { VStack { Button("次へ") { // ← 遷移前の処理 print("遷移前処理") path.append(.next) // ← 型の値を push } } // 遷移先の画面定義 .navigationDestination(for: MyPage.self) { page in switch page { case .next: NextView() } } } } } |
👍 画面に応じてパラメータも持たせられる
例えば「ID を持つ詳細画面」が必要なら:
|
1 2 3 4 |
enum MyPage: Hashable { case next case detail(id: Int) } |
遷移:
|
1 |
path.append(.detail(id: 5)) |
受け取り:
|
1 2 3 4 5 6 7 8 |
.navigationDestination(for: MyPage.self) { page in switch page { case .next: NextView() case .detail(let id): DetailView(id: id) } } |
柔軟で超使いやすいです。
🎯 まとめ
MyPage は—
enumHashable- case に遷移先を列挙
という形で宣言します。
これが SwiftUI の NavigationStack の“正しい使い方”です。
✅ .sheet で画面遷移した場合
遷移元の View(親 View)は “生きています”!
.sheetは モーダル表示(上に被せるだけ)- 親 View は 破棄されません
onAppearも再度呼ばれません@Stateも@StateObjectもそのまま保持される
つまり、
・Sheet が上に乗るだけ
・元の画面は裏でそのまま動き続ける
🧪 動作例(簡単なイメージ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct ParentView: View { @State private var count = 0 @State private var showSheet = false var body: some View { VStack { Text("count: \(count)") Button("count + 1") { count += 1 } Button("Open Sheet") { showSheet = true } } .sheet(isPresented: $showSheet) { SheetView() } } } |
この時:
- Sheet を開いても count は維持される
- Sheet を閉じても count はそのまま
原因:ParentView が破棄されていないから。
| 遷移方法 | 親Viewの状態 | 再描画 | 生存状態 |
|---|---|---|---|
.sheet | 生きてる | sheetが出ても親はそのまま | 生きてる |
.fullScreenCover | 生きてる | 同上 | 生きてる |
| NavigationStack push | 生きてる | 表示外で省エネされることはある | 生きてる |
| NavigationStack pop | → 破棄される | 戻る → 再生成 | 破棄されて新しいのが作られる |
🟣 .sheet の重要な注意点
1. .sheet 内側の View は毎回再生成される(ほぼ確実)
|
1 2 3 |
.sheet { MySheetView() // ← ここは開くたびに新しく作られる } |
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を変えないようにする)
例:
|
1 2 3 4 5 |
if obj.flag { ViewA() } else { ViewA().environment(\.someSetting, false) } |
のように「View を変えずに中身だけ変える」ことで、
View の identity を安定させる。
❗ 解決策③
🔹 NavigationStack を使い、push/pop で構造を安定させる
親画面の body 再描画で identity が変わる問題が減る。
❗ 解決策④
🔹 sheet は “親View の一番外側” につける
(内部の条件分岐の外に出す)
これも identity を安定させる効果が高い。
❗ 解決策⑤
🔹 ObservableObject を親Viewの外で保持(@StateObject の位置を変える)
|
1 2 3 4 5 6 7 |
struct ParentScreen: View { @StateObject private var vm = AppViewModel() var body: some View { ParentView(vm: vm) } } |
こうすると ParentView が再構築されても ViewModel が保たれるため、
画面構造がある程度安定しやすい。
今回の自分の対応。
そもそも設定を行うViewのフラグ変数だったため新たにそのViewの管理クラスを作り、そこにフラグ変数を持たせた。(アプリの機能追加で起きた問題だったが、規模が小さいアプリだからといって1つのObservableObjectクラスに共通変数を全部管理させていたのが元凶)
✔️ NavigationStack で画面遷移した場合
基本ルール:
ObservableObject(@StateObject / @EnvironmentObject)は画面遷移後も状態を保持します。
つまり:
- NavigationStack の push(画面遷移)
- pop(戻る)
- 深い階層への遷移
いずれでも ObservableObject のインスタンスは再生成されません。
→ 状態は維持されます。
✔️ ただし例外ポイントがある(ここが重要)
① 親Viewで @ObservedObject を使っている場合
もしこんな構造になっていると…
|
1 2 3 |
struct ParentView: View { @ObservedObject var sv = SharedVariables() // ←ここに注意 } |
これは 遷移のたびに再生成される可能性がある ので NG。
保持したいなら必ず:
@StateObject を使う
|
1 |
@StateObject var sv = SharedVariables() |
もしくは上位の Scene/RootView で作って .environmentObject で渡す。
② NavigationStack 自体を再生成した場合
もし NavigationStack を含む親 View が再描画で作り直されると、その中で作られている状態は初期化されます。
例:
|
1 2 3 4 5 6 7 |
var body: some View { if sv.showMain { NavigationStack { MainView() } } } |
こういうパターンだと 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再描画 |
✔️ よくある安全パターン
- SharedVariables を @StateObject で RootView に置く
- NavigationStack は RootView 直下に固定(条件分岐で消さない)
- 子画面にも environmentObject で渡す
例:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@main struct MyApp: App { @StateObject var sv = SharedVariables() var body: some Scene { WindowGroup { NavigationStack { RootView() } .environmentObject(sv) } } } |
こうすると 状態がほぼ永続的に安定 します。
画面遷移なしでも View を再構築(= onAppear を再発火)
🔥 最も簡単な方法:View の “identity” を変える
SwiftUI では id が変わると View は完全に別物扱いになる ため、再生成されて onAppear が再度発火します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct MyView: View { @State private var refreshID = UUID() var body: some View { VStack { Text("内容") } .id(refreshID) // ← これがポイント .onAppear { print("onAppear 発火!") } Button("再描画する") { refreshID = UUID() // ← ID を変えると再生成される } } } |
この refreshID = UUID() を実行すると:
- View は完全に再生成される
onAppearがもう一度発火する- アニメーションやレイアウトも初期化される
🟦 他の方法(状況に応じて使える)
① 親 View で条件分岐して再描画させる
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@State private var show = true var body: some View { VStack { if show { ChildView() } else { EmptyView() } Button("再描画") { show.toggle() } } } |
show.toggle() により ChildView が一瞬消えて再度生成 → onAppear 発火。
② NavigationStack で path を操作する(疑似リセット)
もし NavigationStack を使っているなら:
|
1 2 3 4 5 |
@State private var path: [String] = [] .onAppear { path = [] // ← スタックをリセット } |
これでスタックのトップ画面が再度構築される。
🟣 「onAppear を再発火させるべきか?」も注意ポイント
SwiftUI的には以下がベストプラクティスです👇
- 副作用を onAppear に詰め込みすぎない
(本来は View の「初回表示」だけでやるべき処理) - リロードは ViewModel に責務を寄せるほうが安全
→ View の lifecycle に依存しないためバグが減る
とはいえ、
「画面遷移なしで View をリセットしたい」
というケースは多いので .id() を使う方法はとても有効です。
✅ 遷移「する時」に処理を実行する方法
① navigationDestination で遷移前に処理を走らせる(オススメ)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
NavigationStack { Button("次へ") { // ← 遷移以外の処理 SV.saveTodayButton() SV.resetSomething() path.append(MyPage.self) } .navigationDestination(for: MyPage.self) { _ in NextView() } } |
✔ 遷移する瞬間に任意の処理が確実に実行
✔ NavigationStack と相性が最高
✔ 遷移ボタン内に書くので分かりやすい
② カスタム init() を使って遷移時に処理する
遷移先の View がロードされた瞬間に処理を走らせたい場合。
|
1 2 3 4 5 6 7 8 9 |
struct NextView: View { init() { print("NextView init → 遷移直後に呼ばれる処理") } var body: some View { Text("Next") } } |
✔ 遷移先が生成された瞬間に一度だけ実行
✔ onAppear より確実(onAppear は再描画だけで呼ばれる)
③ 遷移先の onAppear を使う
よく使われるパターン。
|
1 2 3 4 |
NextView() .onAppear { print("遷移して画面が見えた瞬間に呼ばれる") } |
✅ 遷移「から戻る時」に処理を実行する方法
NavigationStack の戻り検知はいくつかやり方があります。
① onDisappear を使う(直感的)
|
1 2 3 4 5 6 7 8 |
struct NextView: View { var body: some View { Text("Next") .onDisappear { print("戻るときに実行される") } } } |
✔ NavigationStack の戻るボタンでも呼ばれる
✔ sheet と違って “ほぼ確実に呼ばれる”
② NavigationStack の path を監視する(最も SwiftUI 的)
パスに要素が追加されたり削除されたりするのを見張る。
|
1 2 3 4 5 6 7 8 9 10 |
@State var path: [MyPage] = [] NavigationStack(path: $path) { // ... } .onChange(of: path) { newValue in if !newValue.contains(.next) { print("NextView から戻ってきた!") } } |
✔ 大規模アプリに向く
✔ 画面戻りだけ確実に検知できる
③ 遷移先 View に Binding を渡して戻り処理を起こす
|
1 2 3 |
NextView(onExit: { SV.saveSetting() }) |
遷移先で:
|
1 2 3 4 5 6 7 8 9 10 |
struct NextView: View { let onExit: () -> Void var body: some View { Text("戻る") .onDisappear { onExit() } } } |
✔ “戻るとき専用の処理” を View に依存せず書ける
✔ ロジックの依存を減らせる
🎯 まとめ(最もおすすめ構成)
| タイミング | ベストアプローチ |
|---|---|
| 遷移する時の処理 | 遷移ボタン内で実行 or init() |
| 遷移先が表示された時の処理 | onAppear |
| 戻ってきた時の処理 | onDisappear or path監視 |
SwiftUI では「戻り時の処理」だけ少しクセがありますが、
上の方法なら確実に処理できます。

コメント