Reputation: 10674
I am having trouble understanding why SwiftUI list selection behaviour is different between the Previews and the actual app running when pushing a detail view.
Here is the app view
import SwiftUI
@main
struct SwiftUIListApp: App {
var body: some Scene {
WindowGroup {
ListView()
}
}
}
and here is my simple list
import SwiftUI
struct ListItem: Identifiable, Hashable {
let title: String
var id: String { title }
}
@Observable
final class ListViewModel {
private(set) var items: [ListItem] = []
@MainActor func loadData() async {
items = (1...100).map { ListItem(title: "\($0)") }
}
}
struct ListView: View {
@State private var viewModel = ListViewModel()
var body: some View {
NavigationStack {
List {
ForEach(viewModel.items) { item in
NavigationLink(value: item) {
Text(item.title)
}
}
}
.navigationTitle("SwiftUI List")
.navigationDestination(for: ListItem.self) { item in
ListDetailView(item: item)
}
}
.task {
await viewModel.loadData()
}
}
}
struct ListDetailView: View {
let item: ListItem
var body: some View {
Text("Detail view \(item.title)")
.navigationTitle("Detail")
.navigationBarTitleDisplayMode(.inline)
}
}
#Preview {
ListView(viewModel: ListViewModel())
}
When I select a row in the preview, the row gets selected and the detail view is pushed. When I navigate back to the list the row gets de-selected when the list view appears. This is similar behaviour to UITableViewController and is exactly what I want. (https://developer.apple.com/documentation/uikit/uitableviewcontroller/1614758-clearsselectiononviewwillappear)
However when I run the app in the simulator/device, the row gets deselected immediately before the push. The list view itself does not seem to re-draw because scroll position is maintained correctly.
Upvotes: 0
Views: 98
Reputation: 10674
Found the solution after playing around with Apples demo app https://developer.apple.com/documentation/swiftui/bringing_robust_navigation_structure_to_your_swiftui_app
Added this to the view model
var itemPaths: [ListItem] = []
And updated the NavigationStack
NavigationStack(path: $viewModel.itemPaths) { ... }
Still not sure why the preview behaves differently but I take it.
Upvotes: 0
Reputation: 273540
It is usually the Preview that is broken, and in this case, you don't control the selection with a selection:
parameter, so it is reasonable that the List
doesn't track its selection.
To make the List
selectable, do:
@State var selection: ListItem?
var body: some View {
NavigationStack {
List(selection: $selection) {
...
}
.navigationDestination(item: $selection) { item in
ListDetailView(item: item)
}
}
}
Note that I have changed this to use navigationDestination(item:destination:)
. If you would like to track navigation with a navigation path, it is less convenient, but still doable.
@State var navigationPath: [ListItem] = []
var body: some View {
NavigationStack(path: $navigationPath) {
List(selection: $navigationPath[safe: 0]) {
where the [safe:]
subscript is:
extension MutableCollection where Self: RangeReplaceableCollection {
subscript(safe i: Index) -> Element? {
get { indices.contains(i) ? self[i] : nil }
set {
if let newValue {
if indices.contains(i) {
self[i] = newValue
} else {
self.insert(newValue, at: i)
}
} else {
remove(at: i)
}
}
}
}
Upvotes: 1