How to Programmatically Pre-select Cells in a Multi-select UITableView in iOS

Viewed 256

I use a UITableView to let the user select from a number of given options with multiple selections allowed. I also want the user to later return to this view and change a previously made selection, which means that I must be able to load and initialize the table with the earlier selections. Furthermore, the user can tap a "select-all" button, which should programmatically set all options.

To do this, I have an array of booleans that keeps track of checked and unchecked items. However, in order to correctly trigger didSelectRowAt and didDeselectRowAt events, the table view also needs to be aware of the selection status. So I came up with two options, neither of which I'm entirely happy with:

Setting the cell accessory types using my own array:

func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return items.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    let cell: UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)
    cell.textLabel!.text = items[indexPath.row].name
    if items[indexPath.row].isSelected {
        cell!.accessoryType = .checkmark
    } else {
        cell!.accessoryType = .none
    }
    return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
    let cell = tableView.cellForRow(at: indexPath)
    cell!.accessoryType = .checkmark
    ...
}

func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath)
    cell!.accessoryType = .none
    ...
}

This nicely toggles the states and updates the backing array. What it fails to do is keeping the table view aware of the selection state of each cell. So it happens that, on reload, the wrong selection event (did select/did deselect) is triggered and then requires the user to initially tap twice for changing the state. Now I could work around this by handling both statuses in both didSelectRowAt and didDeselectRowAt but it contradicts the status of the control and may later lead to problems.

Letting the table view keep track of the state:

Here, I replaced

if isSelected(index: indexPath.row) {

with

if let selectedRows = tableView.indexPathsForSelectedRows, selectedRows.contains(indexPath) {

This keeps the table view internally updated but I have not found a good way to programmatically set the state when the user returns to the table with some pre-selected items or when "select-all" is clicked. Trying to iterate through my array and setting the selections using e.g.

`tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none)`

(as suggested in answers to similar questions) where applicable did not lead to the desired result.

What would be the best way to initialize my table with pre-selected values and update it when "select-all" is clicked?

1 Answers

You should manage it entirely using your data model.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath)

    // Assumption: item is a class, so changes are reflected in array as expected
    let item = items[indexPath.row]
    item.isSelected.toggle()

    cell!.accessoryType = item.isSelected ? .checkmark : .none
}

This way there's always one source of truth, your data model. Your tableView instance should not need to remember anything for you, it's driven by the data you provide.

If you go this way, you don't need to implement didDeselect delegate method OR set allowsMultipleSelection to true.

Related