はじめに
この記事は ディップAdvent Calendar 2025の投稿です。
iOS 18.3 から 18.4 にかけて発生・改善が確認された SwiftUI Navigation 周りの不具合についてまとめます。
なぜこの記事を書いたのか
iOS 18 系では SwiftUI の NavigationStack / navigationDestination 周りで
「コードは正しいのに挙動がおかしい」
「特定の OS / Xcode 組み合わせでのみ再現する」
といった問題に遭遇した方も多いのではないでしょうか?
私自身も同様の問題に直面し、原因の切り分けや回避策の検討にかなり苦労しました。。
特に iOS 18.3 までの環境では、
- navigationDestination が無限ループする
- dismiss が二重に発火する
- 遷移が不安定でデバッグが困難
といった不具合が複数報告されていました。
本記事では、iOS 18.3 までに報告されていた SwiftUI Navigation 周りの問題と、
iOS 18.4 / 18.4.1 系で改善が確認された点を整理します。
公式リリースノートだけでは分かりづらい
「実際の開発現場で何が起きて、どう変わったのか」
を中心にまとめています。
❌ iOS 18.3 まで問題が起きやすかった例(Bool ベース)
struct RootView: View {
@State private var showDetail = false
var body: some View {
NavigationStack {
Button("Go Detail") {
showDetail = true
}
.navigationDestination(isPresented: $showDetail) {
DetailView()
}
}
}
}
このような Bool ベースの navigationDestination(isPresented:) は、
公式ドキュメント上もよく紹介されている書き方です。
しかし、iOS 18.3 までの環境では、
@State の更新と Navigation の評価タイミングが絡むことで
意図しない挙動が発生するケースがありました。
⚠️発生していた問題
- iOS 18.3 環境で
- 画面遷移がループする
- 遷移後にフリーズする
- 特に以下の条件で再現しやすい
- onAppear / onDisappear 内で State を更新している
- 親 View の再描画が頻繁に発生する構成
- 非同期処理と組み合わさった場合
コード上はシンプルでも、
内部的には Navigation の再評価が連鎖的に発生してしまうケースがありました。
✅ iOS 18.4 / 18.4.1 で改善された点
iOS 18.4 / 18.4.1 環境では、
同じコードでも 無限ループが再現しなくなった という報告が増えています。
- フリーズや重複遷移の発生率が大きく低下
- Bool ベース Navigation でも安定するケースが増加
これは、SwiftUI の NavigationStack 周りで
View 更新時の environment 管理や再評価順序が改善された影響と考えられます。
※ ただし、Navigation 構成が複雑な場合やState 管理が密結合になっている場合は、
依然として不安定になるケースもあります。
🔁 推奨:path ベース Navigation(安定)
struct RootView: View {
@State private var path: [Route] = []
var body: some View {
NavigationStack(path: $path) {
Button("Go Detail") {
path.append(.detail)
}
.navigationDestination(for: Route.self) { route in
switch route {
case .detail:
DetailView()
}
}
}
}
}
path ベースの Navigation は、
状態(path)と遷移が明確に対応するため、
内部的な再評価の影響を受けにくい構成になります。
✍️この構成のメリット
-
Bool ベースより挙動が安定
-
iOS 18.3 / 18.4 両対応しやすい
-
State と遷移の責務が明確
-
テスト・デバッグがしやすい
❌ dismiss が不安定だった例(18.3)
struct DetailView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
Button("Back") {
dismiss()
}
}
}
⚠️ 発生していた問題
-
一度の操作で複数回 dismiss() が呼ばれたような挙動
-
想定より深く pop されることがあった
-
NavigationStack の深さによって再現性が変わる
こちらも、
View のライフサイクルと Navigation の再評価がズレることが
原因と考えられます。
✅ 18.4 系での状況
iOS 18.4 / 18.4.1 では、
- 二重 dismiss の報告が大幅に減少
- 単純な Navigation 構成では安定して動作
といった改善が確認されています。
※ ただし、複数の NavigationStack がネストしている場合や、
State 管理が複雑な場合には、引き続き注意が必要です。
まとめ
iOS 18.3 までに報告されていた SwiftUI Navigation 周りの致命的な不具合は、
iOS 18.4 / 18.4.1 系でかなり改善された印象があります。
一方で、NavigationStack は内部実装の変更が入りやすい領域でもあり、
OS / Xcode の組み合わせ差分による影響は今後も注意が必要です。
本記事が、同様の問題に直面している方の切り分けや設計判断の参考になれば幸いです。
参考リンク
本記事の内容は、以下の公式情報および開発者コミュニティでの報告・検証記事をもとに整理しています。
📘 Apple 公式リリースノート
🛠 開発者コミュニティでの改善報告
SwiftUI navigation bug in iOS 18.3 → fixed in iOS 18.4.1
🔎 Navigation 設計に関する背景 Issue
navigationDestination(isPresented:) の挙動に関する Issue(参考)