Adding context menus to rows in SwiftUI's Table on macOS
Published on 26 October 2022
When I was working on the tables of certificates, devices, bundle IDs and provisioning profiles in AppDab, I stumbled upon a missing feature in SwiftUI on macOS: It is not possible to add a context menu to the full row in a Table
.
One could expect, that the right thing to do, was to just add the .contextMenu
to the Table
, but sadly it doesn't do anything. It is possible to add the context menu to the content of the column, but doing so, the context menu is only triggered when right-clicking on the Text
in the column and not the full cell.
import SwiftUI
struct BundleIdsListView: View {
@State var items: [BundleIdViewModel]
var body: some View {
Table(items) {
TableColumn("Name", value: \.name) {
Text($0.name)
.contextMenu(
ContextMenu(menuItems: {
Button("Rename", action: { ... })
Button("Delete", action: { ... })
})
)
}
}
}
}
The "solution"
After a lot of researching and experiments I found a thread on Apple Developer Forums that lead me to something. It is not pretty and doesn't work as well as other AppKit apps, but the user can right-click on the whole width of the row in the table.
So make it work this way, we need to make the Text
in the column fill all of the available space and add a .contentShape
like this:
import SwiftUI
struct BundleIdsListView: View {
@State var items: [BundleIdViewModel]
var body: some View {
Table(items) {
TableColumn("Name", value: \.name) {
Text($0.name)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.contentShape(Rectangle())
.contextMenu(
ContextMenu(menuItems: {
Button("Rename", action: { ... })
Button("Delete", action: { ... })
})
)
}
}
}
}
Simplification
But when we have multiple columns we will now have the workaround and the context menu duplicated for every column. To simplify this, I added a view modifier which takes the ContextMenu
and applies it along with the workaround.
import SwiftUI
public extension View {
func tableColumnContextMenu<MenuItems>(_ contextMenu: ContextMenu<MenuItems>?) -> some View where MenuItems: View {
self
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.contentShape(Rectangle())
.contextMenu(contextMenu)
}
}
Conclusion
With the view modifier it is possible to just create the context menu in a function and pass it along. This is the solution that we have to live with until SwiftUI gets a better API for context menus in Table
s.
import SwiftUI
struct BundleIdsListView: View {
@State var items: [BundleIdViewModel]
var body: some View {
Table(items) {
TableColumn("Name", value: \.name) {
Text($0.name)
.tableColumnContextMenu(createContextMenu(for: $0))
}
TableColumn("Identifier", value: \.identifier) {
Text($0.identifier)
.tableColumnContextMenu(createContextMenu(for: $0))
}
TableColumn("Type", value: \.platform) {
Text($0.platform)
.tableColumnContextMenu(createContextMenu(for: $0))
}
}
}
private func createContextMenu(for bundleId: BundleIdViewModel) -> ContextMenu<TupleView<(Button, Button)>> {
ContextMenu {
Button("Rename", action: { ... })
Button("Delete", action: { ... })
}
}
}
Tagged with: