In the executable code below I try to implement the main navigation view for an (iPadOS) app. The expected behaviour is as follows:
- A NavigationSplitView is used to present a sidebar to the user containing the main categories of the app
- If a user selects a category, in the detail view of the NavigationSplitView a NavigationStack is presented
- The navigation stack allows users to interact with the specific part of the app associated with the category by pushing other views on the navigation stack
- If a user switches goes to category A, pushes some views on A's NavigationStack, switches to category B and then switches back to category A, it is expected that the NavigationStack of category A contains all the views, the user pushed there earlier
However, (4) is not working. Every time a user selects another category in the sidebar, the navigation paths of the associated NavigationStack becomes empty. For example, if a user pushes three views on the view stack of category A, then uses the sidebar to select category B, the navigation paths of category A become empty and the NavigationStack of category B is displayed.
I kindly ask for help as I cannot find the issue in the code.
The following code demonstrates the issue:
import SwiftUI
/// Add this view to your app to test the behavior.
///
/// This view displays a sidebar with four entries (RootCategory enum).
/// Each detail view has a NavigationStack that referes to one of the Routing
/// state objects from the RootNavigation.
///
/// The goal is that users can switch the categories in the sidebar without losing
/// their navigation stack of their respective child view once they are going back
/// to a previously selected category.
///
/// However, this does not work because everytime a different categor is selected from
/// the sidebar, the navigation paths of the ChildView's routing is emptied. (see line 82).
struct RootNavigation: View {
@StateObject var categoryOneRouting = Routing()
@StateObject var categoryTwoRouting = Routing()
@StateObject var categoryThreeRouting = Routing()
@StateObject var categoryFourRouting = Routing()
@State var navigationSelection: RootCategory? = .category1
var body: some View {
NavigationSplitView {
List(RootCategory.allCases, selection: $navigationSelection) { category in
NavigationLink(category.name, value: category)
}
.navigationTitle("App Title")
} detail: {
switch navigationSelection {
case .category1:
ChildView(title: RootCategory.category1.name)
.environmentObject(categoryOneRouting)
case .category2:
ChildView(title: RootCategory.category2.name)
.environmentObject(categoryTwoRouting)
case .category3:
ChildView(title: RootCategory.category3.name)
.environmentObject(categoryThreeRouting)
case .category4:
ChildView(title: RootCategory.category4.name)
.environmentObject(categoryFourRouting)
case nil:
Text("Selection does not exist")
}
}
}
}
struct ChildView: View {
let title: String
@EnvironmentObject var routing: Routing
var body: some View {
NavigationStack(path: $routing.routes) {
VStack {
Text(title)
.navigationTitle(title)
Button("Screen 1") {
routing.routes.append(.subview1)
}
Button("Screen 2") {
routing.routes.append(.subview2)
}
Button("Screen 3") {
routing.routes.append(.subview3)
}
}
.navigationDestination(for: Route.self) { route in
Text(route.rawValue)
}
}
.buttonStyle(.borderedProminent)
}
}
class Routing: ObservableObject {
@Published var routes: [Route] = [] {
willSet {
// when selecting another category in the sidebar, the navigation paths of
// the currently visible NavigationStack will be set to empty. Why and how
// can I prevent it from losing the navigation paths of NavigationStacks
// that disappear?
if newValue.isEmpty {
print("Navigation paths just got emptied")
}
}
}
}
enum RootCategory: Int, CaseIterable, Identifiable {
case category1
case category2
case category3
case category4
var id: Int { self.rawValue }
var name: String {
switch self {
case .category1: return "Category 1"
case .category2: return "Category 2"
case .category3: return "Category 3"
case .category4: return "Category 4"
}
}
}
enum Route: String {
case subview1
case subview2
case subview3
}
#Preview {
return RootNavigation()
}