crashoverride777
crashoverride777

Reputation: 10674

SwiftUI List selection behaviour Preview vs Simulator

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

Answers (2)

crashoverride777
crashoverride777

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

Sweeper
Sweeper

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

Related Questions