swiftsortingswiftuiswiftui-table

How to create a sortable and editable SwiftUI Table?


I want to create a sortable Table where each table cell is a TextField so that the cell contents can be edited.

Let's start with this unsortable Table that has only one column. In the real code, this is of course going to have more columns and rows can be added and removed too.

struct ContentView: View {
    @State var rows = [
        Row(name: "A"),
        Row(name: "B"),
    ]

    var body: some View {
        Table($rows) {
            TableColumn("Name") { $row in
                TextField("Enter Name", text: $row.name)
            }
        }
    }
}

struct Row: Identifiable, Hashable {
    let id = UUID()
    var name: String
}

To make this sortable, I would need to pass a sortOrder: binding to Table, as well as well as manually sorting the rows array in onChange(of: sortOrder) { ... }.

However, this doesn't work:

@State var sortOrder: [KeyPathComparator<Binding<Row>>] = []

var body: some View {
    Table($rows, sortOrder: $sortOrder) {
        TableColumn("Name", value: \.wrappedValue.name) { $row in
            TextField("Enter Name", text: $row.name)
        }
    }
    .onChange(of: sortOrder) { _, sortOrder in
        rows.sort(using: sortOrder) // error
    }
}

Instance method sort(using:) requires the types Row and Binding<Row> be equivalent

I understand that this is because I am trying to sort an array of Rows using sort comparators that compare Binding<Row>s.

However, the Table initialiser requires that the type of sortOrder: to be

Binding<[KeyPathComparator<Binding<Row>>]>

because this is a table of Binding<Row>s.

How can I sort the rows in onChange? Perhaps there is a way to convert [KeyPathComparator<Binding<Row>>] to [KeyPathComparator<Row>]?


Just in case this is relevant, I only want the table to be re-sorted when sortOrder changes. I do not want the table to be re-sorted when some row's name is edited.


Solution

  • You could try this approach using a dedicated Binding in the TextField, rather than having a $rows in the Table.

    Example code:

    
    struct ContentView: View {
        @State private var rows = [Row(name: "A"), Row(name: "B")]
        @State private var sortOrder = [KeyPathComparator(\Row.name)] // <-- here
        
        var body: some View {
            Table(rows, sortOrder: $sortOrder) {  // <-- here
                TableColumn("Name", value: \Row.name) { row in
                    TextField("Enter Name", text: Binding(  // <-- here
                        get: { row.name },
                        set: {
                            if let index = rows.firstIndex(of: row) {
                                rows[index].name = $0  // <-- here
                            }
                        }))
                }
            }
            .onChange(of: sortOrder) {
                rows.sort(using: sortOrder)
            }
        }
    }
    
    struct Row: Identifiable, Hashable {
        let id = UUID()
        var name: String
    }