I have a NavigationSplitView with a Sidebar and Detail view. When selecting an item from the sidebar, the Detail view is updated to show what was selected. This all works fine so far on both an iPhone and an iPad.
However, on an iPhone, when returning back to the sidebar view to select something else, the previously selected item is being set to nil.
On an iPad it is not setting the previously selected item to nil, even if you hide and show the sidebar, it still remembers the previously selected entry.
I have tried with the iPhone simulator and on a physical device too.
To reproduce the issue, see below small (complete) code. Run it on a simulator or physical device. Select an entry from the sidebar, the detail is shown/updated showing what was selected. Return back to the sidebar view and you can see for about half a second the previously selected item still selected, then it gets set to nil.
Why is the selection being reset back to nil on the iPhone when returning back to the Sidebar view?
import SwiftUI
struct ContentView: View {
@State private var selection: String?
var body: some View {
NavigationSplitView {
SidebarView(selection: $selection)
} detail: {
DetailView(selection: $selection)
}
}
}
struct SidebarView: View {
@Binding var selection: String?
let people = ["Finn", "Leia", "Luke", "Rey"]
var body: some View {
List(people, id: \.self, selection: $selection) { person in
Text(person)
}
Text("selection = \(String(describing: selection))")
}
}
struct DetailView: View {
@Binding var selection: String?
var body: some View {
Text("selectedItem = \(String(describing: selection))")
}
}
#Preview {
ContentView()
}
See below GIF. Notice down the bottom of the screen of the sidebar. When returning to the sidebar you will see it still shows the previously selected item for a very short half a second or so. Then it gets cleared to nil.
Just make the @State
a non-optional. Then convert it to Binding<String?>
using one of its initialisers, before passing it to selection:
struct ContentView: View {
@State private var selection: String = "Finn"
var body: some View {
NavigationSplitView {
SidebarView(selection: $selection)
} detail: {
DetailView(selection: $selection)
}
}
}
struct SidebarView: View {
@Binding var selection: String
let people = ["Finn", "Leia", "Luke", "Rey"]
var body: some View {
// note the selection parameter
List(people, id: \.self, selection: Binding($selection)) { person in
Text(person)
// if you want to indicate the previously selected person more clearly
.listRowBackground(selection == person ? Color.gray : nil)
}
Text("selection = \(String(describing: selection))")
}
}
struct DetailView: View {
@Binding var selection: String
var body: some View {
Text("selectedItem = \(String(describing: selection))")
}
}
This forces you to give an initial selection. If you don't want that, you can add a new @State
, in SidebarView
or ContentView
.
struct SidebarView: View {
@Binding var selection: String?
@State private var visitedPerson: String?
let people = ["Finn", "Leia", "Luke", "Rey"]
var body: some View {
Group {
List(people, id: \.self, selection: $selection) { person in
Text(person)
// if you want to indicate the previously selected person more clearly
.listRowBackground(visitedPerson == person ? Color.gray : nil)
}
Text("selection = \(String(describing: visitedPerson))")
}
.onChange(of: selection) { oldValue, newValue in
if let newValue {
visitedPerson = newValue
}
}
}
}
That said, the "forced deselection" behaviour is consistent with how iOS behaves everywhere else. When a view is pushed on top of a List
and then subsequently popped, the List
will not retain its selection.
See for example:
struct ContentView: View {
@State private var items = ["One", "Two", "Three"]
@State private var selected: String?
var body: some View {
NavigationStack {
List(items, id: \.self, selection: $selected) { item in
Text(item)
}
.toolbar {
NavigationLink("Navigate") {
Text("Destination")
}
}
}
}
}
You can also compare the Settings app on iPhones and iPads, which also has a NavigationSplitView
-like design.