macOS SwiftUI Table with contextMenu

Viewed 253

Using SwiftUI's new Table container, how can I add a context menu that appears when Control-clicking a row?

I can add the contextMenu modifier to the content of the TableColumn, but then I will have to add it to each individual column. And it only works above the specific text, not on the entire row:

Where the context menu works, and where it doesn't

I tried adding the modifier to the TableColumn itself, but it shows a compile error:

Value of type 'TableColumn<RowValue, Never, Text, Text>' has no member 'contextMenu'

Here's what I have in terms of source code, with the contextMenu modifier in the content of the TableColumn:

struct ContentView: View {

    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)])
    private var items: FetchedResults<Item>

    @State
    private var sortOrder = [KeyPathComparator(\Item.name)]

    @State
    private var selection = Set<Item.ID>()

    var body: some View {
        NavigationView {
            Table(items, selection: $selection, sortOrder: $items.sortDescriptors) {
                TableColumn("Column 1") {
                    Text("Item at \($0.name!)")
                        .contextMenu {
                            Button(action: {}) { Text("Action 1") }
                            Divider()
                            Button(action: {}) { Text("Action 2") }
                            Button(action: {}) { Text("Action 3") }
                        }
                }

                TableColumn("Column 2") {
                    Text($0.id.debugDescription)
                }
            }
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }

            if selection.isEmpty {
                Text("Select an item")
            } else if selection.count == 1 {
                Text("Selected \(items.first(where: { $0.id == selection.first! })!.id.debugDescription)")
            } else {
                Text("Selected \(selection.count)")
            }
        }
    }
}

So, how can I add a context menu to the entire row inside the Table?

2 Answers

You can make cell view with the following extensions and use it for each column cell, then it will be clickable in any row place

Text("Item at \($0.name!)")
    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) // << this !!
    .contentShape(Rectangle())                                            // << this !!
    .contextMenu {
        Button(action: {}) { Text("Action 1") }
        Divider()
        Button(action: {}) { Text("Action 2") }
        Button(action: {}) { Text("Action 3") }
    }

I think the best way to do it is to use this initializer:

init(selection:sortOrder:columns:rows:)

Change your table like this:

Table(selection: $selection, sortOrder: $items.sortDescriptors) {
    TableColumn("Column 1") {
        Text("Item at \($0.name!)")
    }

    TableColumn("Column 2") {
        Text($0.id.debugDescription)
    }
} row: {
    ForEach(items) { item in
        TableRow(item)
            // This line adds drag&drop support and convert a row to some View.
            .itemProvider({ ... }) 
            // Then we apply a context menu to this View.
            .contextMenu {
                Button(action: {}) { Text("Action 1") }
                Divider()
                Button(action: {}) { Text("Action 2") }
                Button(action: {}) { Text("Action 3") }
            }
    }
}

Theoretically this should work for you. But in my situation the compiler yells:

The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions

I think this is a SwiftUI bug or compiler bug.

Related