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 NavigationLink
s.
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.