I'm trying to build an app that has a NavigationSplitView and a NavigationStack in the detail View.
The normal flow works fine, but if I navigate to the second page on the detail view and then select another menu item (i.e. the second item), I'm still on the detail page of the first menu item.
The underlying view of the second detail view changes. This can be observed by the change of the back button label.
How do I ensure that my NavigationStack is also reset when I change the selection?
import SwiftUI
enum Option: String, Equatable, Identifiable {
case first
case second
var id: Option { self }
}
struct ContentView: View {
@State private var selection: Option?
var body: some View {
NavigationSplitView {
List(selection: $selection) {
NavigationLink(value: Option.first) {
Text("First")
}
NavigationLink(value: Option.second) {
Text("Second")
}
}
} detail: {
switch selection {
case .none:
Text("Please select one option")
case .some(let wrapped):
NavigationStack {
DetailView(title: wrapped.rawValue)
}
}
}
.navigationSplitViewStyle(.balanced)
}
}
struct DetailView: View {
private var title: String
init(title: String) {
self.title = title
}
var body: some View {
List {
NavigationLink {
Text(title)
} label: {
Text("Show \(title) detail")
}
}
.navigationTitle(title)
}
}
Hi Lars_, thanks for this thorough question, including the gif. This is undefined behavior we weren't aware of, in fact, it behaves as you'd expect on macOS, but each behavior is arguably correct.
I've filed a radar on our end tracking it with this code sample to make this more consistent.
There are a few ways to handle this. You could set up some plumbing and call dismiss() in DetailView when selection changes. But since you're targeting at least iOS 16 (because NavigationSplitView/Stack are first available there), you can get more programmatic control over your navigation state by using value-based NavigationLinks.
private struct DetailView: View {
private var title: String
init(title: String) {
self.title = title
}
var body: some View {
List {
NavigationLink("Show \(title) detail", value: title)
}
.navigationDestination(for: String.self, destination: { title in
Text(title).font(.headline)
})
.navigationTitle(title)
}
}
Even if no path parameter is provided to a NavigationStack, value-based navigation links will append to an implicit path tracked for you by the Navigation system. Where view-based navigation links will not. When selection changes in the sidebar, the navigation system pops value-based links off the stack.
You'll notice if you use the above example, the stack pop will be animated, which may not be what you want. To disable that, pass a non-animated transaction to the selection binding:
private struct ContentView: View {
@State private var selection: Option?
var nonAnimatedTransaction: Transaction {
var t = Transaction()
t.disablesAnimations = true
return t
}
var body: some View {
NavigationSplitView {
List(selection: $selection.transaction(nonAnimatedTransaction)) {
NavigationLink(value: Option.first) {
Text("First")
}
NavigationLink(value: Option.second) {
Text("Second")
}
}
} detail: {
switch selection {
case .none:
Text("Please select one option")
case .some(let wrapped):
NavigationStack {
DetailView(title: wrapped.rawValue)
}
}
}
.navigationSplitViewStyle(.balanced)
}
}
For even more control, pass a path argument to NavigationStack and you can manage that completely yourself.