NavigationStackの予期せぬ挙動とその回避策
iOS16からナビゲーション管理手法として登場したNavigationStack
は、ページ遷移をスタックで管理でき、より直感的なコントロールが可能になりました。しかし、まだ歴史が浅く文献も少ないため、特定の使い方で予期せぬ挙動が発生することがあったので、その際に見られた挙動と回避案をご紹介します。
最初に申し上げますが、不具合の原因や、そもそも不具合であるのかについての情報は本記事には記載していません。今回は私が経験した挙動とその対処方法に関する話です。
アプリの前提
私が作成したアプリでは、アプリのトップレベルでTabView
を使用し、各タブでNavigationStack
を用いてページ遷移を管理していました。
アプリ構成のイメージ
サンプルコード
import SwiftUI
@main
struct SimpleApp: App {
var body: some Scene {
WindowGroup {
MainTabView()
}
}
}
struct MainTabView: View {
var body: some View {
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house.fill")
}
SearchView()
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
ProfileView()
.tabItem {
Label("Profile", systemImage: "person.fill")
}
}
}
}
struct HomeView: View {
var body: some View {
NavigationStack {
VStack {
Text("Home View")
.font(.largeTitle)
}
.navigationTitle("Home")
}
}
}
struct SearchView: View {
var body: some View {
NavigationStack {
Text("Search View")
.font(.largeTitle)
.navigationTitle("Search")
}
}
}
struct ProfileView: View {
var body: some View {
NavigationStack {
Text("Profile View")
.font(.largeTitle)
.navigationTitle("Profile")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
print("Settings tapped")
}) {
Image(systemName: "gear")
}
}
}
}
}
}
サンプル画面
Home View
Search View
Profile View
課題と挙動の問題
新たな要件として、アプリの通知やDeep Linkをタップして特定のページに遷移させる機能を追加する必要がありました。当初は、TabView
の外側にNavigationStack
をラップしてその中でパスを操作することで実現できると考えました。しかし、結果的に予期せぬ挙動が発生しました。
以下のような問題が発生しました:
- 上位の
NavigationStack
のスタック操作で遷移した後、戻るボタンを押すとTabView
内の変数が初期化される。 - ナビゲーションタイトルが表示されない。
-
Toolbar
が設定した画面以外でも残り続ける。
問題のコード例
import SwiftUI
@main
struct SimpleApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
MainTabView()
}
}
}
}
class AppState: ObservableObject {
@Published var navigationPath: [AppDestination] = []
func navigateToFullScreen() {
navigationPath.append(.fullScreen)
}
func dismissEventDetail() {
navigationPath.removeLast()
}
}
enum AppDestination: Hashable {
case fullScreen
}
struct MainTabView: View {
@StateObject private var appState = AppState()
var body: some View {
TabView {
NavigationStack(path: $appState.navigationPath) {
Button("Show Full Screen") {
appState.navigateToFullScreen()
}
.navigationTitle("Home")
.navigationDestination(for: AppDestination.self) { destination in
switch destination {
case .fullScreen:
FullScreenView()
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Close") {
appState.dismissEventDetail()
}
}
}
}
}
}
.tabItem {
Label("Home", systemImage: "house.fill")
}
NavigationStack {
Text("Search View")
.navigationTitle("Search")
}
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
NavigationStack {
Text("Profile View")
.navigationTitle("Profile")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
print("Settings tapped")
}) {
Image(systemName: "gear")
}
}
}
}
.tabItem {
Label("Profile", systemImage: "person.fill")
}
}
.environmentObject(appState)
}
}
struct FullScreenView: View {
var body: some View {
VStack {
Text("Full Screen View")
.font(.largeTitle)
.padding()
}
}
}
問題の発生する画面例
上位のNavigationStack
にパスを追加した際
元の画面に戻った際のタイトル消失
Toolbarが他の画面にも残って表示される不具合
回避策:fullScreenCoverの使用
ネストされたNavigationStack
が原因で予期せぬ挙動が発生している可能性があるため、代替案としてfullScreenCover
を使用してページ遷移を管理しました。この方法では、TabView
内部のスタックに影響を与えず、指定した画面をフルスクリーンで表示させています。
修正後のコード例
import SwiftUI
@main
struct SimpleApp: App {
var body: some Scene {
WindowGroup {
MainTabView()
}
}
}
class AppState: ObservableObject {
@Published var showFullScreen = false
func dismissEventDetail() {
showFullScreen = false
}
}
struct MainTabView: View {
@StateObject private var appState = AppState()
var body: some View {
content()
.fullScreenCover(isPresented: $appState.showFullScreen) {
NavigationStack {
FullScreenView()
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Close") {
appState.dismissEventDetail()
}
}
}
}
}
}
@ViewBuilder
private func content() -> some View {
TabView {
NavigationStack {
Button("Show Full Screen") {
appState.showFullScreen = true
}
.navigationTitle("Home")
}
.tabItem {
Label("Home", systemImage: "house.fill")
}
NavigationStack {
Text("Search View")
.navigationTitle("Search")
}
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
NavigationStack {
Text("Profile View")
.navigationTitle("Profile")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
print("Settings tapped")
}) {
Image(systemName: "gear")
}
}
}
}
.tabItem {
Label("Profile", systemImage: "person.fill")
}
}
}
}
struct FullScreenView: View {
var body: some View {
VStack {
Text("Full Screen View")
.font(.largeTitle)
.padding()
}
}
}
挙動の画像
修正後の画面
まとめ
NavigationStack
をネストして使用する場合、まだ想定外の挙動が発生することがあります。本記事ではそのような問題を回避するための一例として、fullScreenCover
を活用した方法を紹介しました。
結局、NavigationStack
のネストによる予期せぬ挙動の原因や、それが不具合であるかどうかについては明確な情報を得ることはできませんでした。このため、同様の問題を経験している方や知見をお持ちの方がいれば、ご連絡していただけると助かります!