I'm trying to build an App with two rows NavigationSplitView. The sidebar is used to have categories and the main section as a table (!) of items. When an item (=row) in this table is doubleClicked I want to navigate to a "detailsView" of that particular item. The first approach of the app used a List where dozens of examples are flying around how to use NavigationLink() and .navigationDestination with it, however I couldn't find any working example for swiftUi Table().
The most logical approach seems to me to use "Table(of: ...) {} rows: {ForEach}" appoach where in the ForEach loop the NavigationLinks could be added. But this didn't work. Below an code example to ease the dicsussion:
struct TableView: View {
@State private var items: [Item] = []
@State private var sorting = [KeyPathComparator(\item.model)]
@State private var selection: item.ID?
var body: some View {
VStack{
Table(of: Item.self, selection: $selection, sortOrder: $sorting) {
TableColumn("Make", value: \.make)
TableColumn("Model", value: \.model)
TableColumn("Date", value: \.date)
} rows: {
ForEach(items) { item in
TableRow(item)
NavigationLink(destination: DetailView(itemId: selection))
}
}
}
However it seems this is not allowed, the compiler throws "No exact matches in reference to static method 'buildExpression'" if I add the line of code with NavigationLink.
You should not use NavigationLink
here, because table rows are not View
s and NavigationLink
s cannot be table rows. Each row of the table consists of multiple View
s, one for each column. Plus, you want to detect double-click.
You should use contextMenu(forSelectionType:menu:primaryAction:)
, and programmatically navigate in the primaryAction
closure. This is what will be called when the user double-clicks a table row.
For example, the table can take a Binding<NavigationPath>
and append the selected item ID to it.
@Binding var path: NavigationPath
Table(of: Item.self, selection: $selection, sortOrder: $sorting) {
TableColumn("Make", value: \.make)
TableColumn("Model", value: \.model)
TableColumn("Date", value: \.date)
} rows: {
ForEach(items) { item in
TableRow(item)
}
}
.contextMenu(forSelectionType: Item.ID.self) { _ in } primaryAction: { items in
guard !items.isEmpty, let selection else { return }
// this will push a new view onto the NavigationStack for each double-click
path.append(selection)
}
Here is a complete example:
struct Item: Identifiable, Hashable {
let id = UUID()
let make: String
let model: String
let date = "\(Date())"
}
struct ContentView: View {
@State var path = NavigationPath()
var body: some View {
NavigationSplitView {
TableView(path: $path)
} detail: {
NavigationStack(path: $path) {
Text("Nothing is selected")
.navigationDestination(for: Item.ID.self) { itemId in
DetailView(itemId: itemId)
}
}
}
}
}
struct DetailView: View {
let itemId: UUID
var body: some View {
Text(itemId.uuidString)
}
}
struct TableView: View {
@State private var items: [Item] = [
Item(make: "A", model: "1"),
Item(make: "B", model: "2"),
Item(make: "C", model: "3"),
Item(make: "D", model: "4"),
]
@State private var sorting = [KeyPathComparator(\Item.model)]
@State private var selection: Item.ID?
@Binding var path: NavigationPath
var body: some View {
VStack{
Table(of: Item.self, selection: $selection, sortOrder: $sorting) {
TableColumn("Make", value: \.make)
TableColumn("Model", value: \.model)
TableColumn("Date", value: \.date)
} rows: {
ForEach(items) { item in
TableRow(item)
}
}
.contextMenu(forSelectionType: Item.ID.self) { _ in } primaryAction: { items in
guard !items.isEmpty, let selection else { return }
path.append(selection)
}
}
}
}
It doesn't have to be value-based navigation. You can also use navigationDestination(item:destination:)
. For example:
struct ContentView: View {
@State var doubleClickedItemId: Item.ID?
var body: some View {
NavigationSplitView {
// change TableView accordingly so that it takes a Binding<Item.ID?>
// and set it in primaryAction
TableView(doubleClickedItemId: $doubleClickedItemId)
} detail: {
NavigationStack {
Text("Nothing is selected")
.navigationDestination(item: $doubleClickedItemId) { itemId in
DetailView(itemId: itemId)
}
}
}
}
}