SwiftUI 3 MacOs Table single selection and double click open sheet
Asked Answered
P

3

5
import SwiftUI

struct ContentView: View {
    
    @State private var items: [ItemModel] = Array(0...100).map { ItemModel(id: $0, title: "item \($0)", age: $0) }
    @State private var selection = Set<ItemModel.ID>()
    @State private var sorting = [KeyPathComparator(\ItemModel.age)]
    
    var body: some View {
        Table(items, selection: $selection, sortOrder: $sorting) {
            TableColumn("id", value: \.id) { Text("\($0.id)") }
            TableColumn("title", value: \.title)
            TableColumn("age", value: \.age) { Text("\($0.age)") }
        }
        .onChange(of: sorting) {
            items.sort(using: $0)
        }
        .font(.caption)
        .frame(width: 960, height: 540)
    }
}

struct ItemModel: Identifiable {
    var id: Int
    var title: String
    var age: Int
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

this is a working example of a Table sorted on Model.age, and support multi selection, I want single selection and open sheet on double click on a row, is that possible? also how do I get the selected item object?

thank you πŸ™

Prebendary answered 14/6, 2021 at 23:43 Comment(1)
To do single selection you just need to change the selection @State to ItemModel.ID? instead of a Set of the same type but I'm also struggling with the other bit, handling clicks to do stuff since there's no .onTap or similar handler for table rows. – Brouhaha
B
5

You must change Set<Value.ID> for Value.ID for only one row selection, and make TapGesture in Text.

@State private var selection = Set<ItemModel.ID>() // <-- Use this for multiple rows selections
@State private var selection : ItemModel.ID? // <--- Use this for only one row selection
struct ContentView: View {
    
    @State private var items: [ItemModel] = Array(0...100).map { ItemModel(id: $0, title: "item \($0)", age: $0) }
    //@State private var selection = Set<ItemModel.ID>() <-- Use this for multiple rows selections
    @State private var selection : ItemModel.ID? // <--- Use this for only one row selection
    @State private var sorting = [KeyPathComparator(\ItemModel.age)]
    @State private var showRow = false
    
    var editRow: some View {
        VStack {
            Text(items[selection!].title)
                .font(.title)
            Text("Selected: \(selection.debugDescription)")
             Button("Dismiss") {
                showRow.toggle()
             }.padding()
        }
        .frame(minWidth:400, minHeight: 400)
    }
    
    var body: some View {
        VStack {
            Table(items, selection: $selection, sortOrder: $sorting) {
                TableColumn("id", value: \.id) {
                    Text("\($0.id)")
                        .onTapGesture(count: 2, perform: {
                            if selection != nil {
                                showRow.toggle()
                            }
                        })
                }
                TableColumn("title") { itemModel in
                    Text(itemModel.title)
                        .onTapGesture(count: 2, perform: {
                            if selection != nil {
                                showRow.toggle()
                            }
                        })
                }
                TableColumn("age", value: \.age) { Text("\($0.age)") }
            }
            .onChange(of: sorting) {
                items.sort(using: $0)
            }
            .font(.caption)
            .frame(width: 960, height: 540)
        }
        .sheet(isPresented: $showRow) {
            editRow
        }
    }
}
Browbeat answered 4/11, 2021 at 17:29 Comment(3)
Great answer. I found I also wanted the double-click to change selection. Instead of testing if selection != nil, I set selection = itemModel.id. – Vardon
Upon further testing, I've found a significant problem with this solution. There's different behaviour when clicking the Text view in a row and clicking the blank space in a row. The tap gesture(s) applied to the Text view override the row's own selection mechanism. – Vardon
I have a similar problem can I ask you some questions? – Devour
O
10

Regarding the double click of a table row: Apple introduced a new context menu modifier contextMenu(forSelectionType:menu:primaryAction:) with SwiftUI 4 at WWDC 2022. With this, a primaryAction can be provided that is performed when the user double clicks on a Table row.

@State private var selection: ItemModel.ID?

var body: some View {
    Table(items, selection: $selection, sortOrder: $sortOrder) {
        TableColumn("id", value: \.id)
        TableColumn("title", value: \.title)
        TableColumn("age", value: \.age)
    }
    .contextMenu(forSelectionType: ItemModel.ID.self) { items in
        // ...
    } primaryAction: { items in
        // This is executed when the row is double clicked
    }
}
Oceangoing answered 25/12, 2022 at 0:3 Comment(0)
B
5

You must change Set<Value.ID> for Value.ID for only one row selection, and make TapGesture in Text.

@State private var selection = Set<ItemModel.ID>() // <-- Use this for multiple rows selections
@State private var selection : ItemModel.ID? // <--- Use this for only one row selection
struct ContentView: View {
    
    @State private var items: [ItemModel] = Array(0...100).map { ItemModel(id: $0, title: "item \($0)", age: $0) }
    //@State private var selection = Set<ItemModel.ID>() <-- Use this for multiple rows selections
    @State private var selection : ItemModel.ID? // <--- Use this for only one row selection
    @State private var sorting = [KeyPathComparator(\ItemModel.age)]
    @State private var showRow = false
    
    var editRow: some View {
        VStack {
            Text(items[selection!].title)
                .font(.title)
            Text("Selected: \(selection.debugDescription)")
             Button("Dismiss") {
                showRow.toggle()
             }.padding()
        }
        .frame(minWidth:400, minHeight: 400)
    }
    
    var body: some View {
        VStack {
            Table(items, selection: $selection, sortOrder: $sorting) {
                TableColumn("id", value: \.id) {
                    Text("\($0.id)")
                        .onTapGesture(count: 2, perform: {
                            if selection != nil {
                                showRow.toggle()
                            }
                        })
                }
                TableColumn("title") { itemModel in
                    Text(itemModel.title)
                        .onTapGesture(count: 2, perform: {
                            if selection != nil {
                                showRow.toggle()
                            }
                        })
                }
                TableColumn("age", value: \.age) { Text("\($0.age)") }
            }
            .onChange(of: sorting) {
                items.sort(using: $0)
            }
            .font(.caption)
            .frame(width: 960, height: 540)
        }
        .sheet(isPresented: $showRow) {
            editRow
        }
    }
}
Browbeat answered 4/11, 2021 at 17:29 Comment(3)
Great answer. I found I also wanted the double-click to change selection. Instead of testing if selection != nil, I set selection = itemModel.id. – Vardon
Upon further testing, I've found a significant problem with this solution. There's different behaviour when clicking the Text view in a row and clicking the blank space in a row. The tap gesture(s) applied to the Text view override the row's own selection mechanism. – Vardon
I have a similar problem can I ask you some questions? – Devour
S
3

Like Adam comments, the other answer has a number of problems with the selection region and response time.

You do have to set var selection as ItemModel.ID? but you also have to handle click actions differently.

It's important to note that this will only work from Big Sur on.

The way I handle different actions for single and double clicks is this:

.gesture(TapGesture(count: 2).onEnded {
    print("double clicked")
})
.simultaneousGesture(TapGesture().onEnded {
    print("single clicked")
})

For your example:

struct ContentView: View {
    
    @State private var items: [ItemModel] = Array(0...100).map { ItemModel(id: $0, title: "item \($0)", age: $0) }
    @State private var selection = ItemModel.ID?
    @State private var sorting = [KeyPathComparator(\ItemModel.age)]
    @State private var isShowingSheet: Bool = false
    
    var body: some View {
        Table(items, selection: $selection, sortOrder: $sorting) {
            TableColumn("id", value: \.id) {
                Text("\($0.id)").gesture(TapGesture(count: 2).onEnded {
                    self.
                }).simultaneousGesture(TapGesture().onEnded {
                    self.selection = $0.id
                })
            }
            TableColumn("title", value: \.title)
            TableColumn("age", value: \.age) { Text("\($0.age)") }
        }
        .onChange(of: sorting) {
            items.sort(using: $0)
        }
        .font(.caption)
        .frame(width: 960, height: 540).sheet(isPresented: self.$isShowingSheet) {
            Button("Close Sheet") { self.isShowingSheet = false } // <-- You may want to allow click to close sheet.
            Text("Sheet Content Here")
        }
    }
}

If you want to allow single and double click in the entire row, you need to have the TableColumn content fill the entire width of the column and apply the modifiers on the rest of the TableColumn contents.

Spiky answered 26/3, 2022 at 18:9 Comment(2)
I have a similar problem can I ask you some questions? – Devour
@Devour SwiftUI Table is relatively new, but happy to help. If it's related, edit the question, so it benefits the community. Otherwise, leave a link to a new question here. – Spiky

© 2022 - 2025 β€” McMap. All rights reserved.