Saturday, September 03, 2022

Unselect cell in tableview if the cell is visible

I have an iOS app. The main screen has a tabbar on the bottom with three tab bar items. Here I'll explain two related tabs. One of them, the “export” tab, is for displaying objects in a tableview. It allows the user to make multiple selections. Another one, the "edit" tab, is for editing objects. If a user makes any changes to objects, any previously selected object items in the export tab will be cleared if the user switches back to the export tab.

Multiple Selection in Export View

In order to keep selected objects consistant, I created a class called SelcHelper to catch selected objects' indexPath. In ExportViewController, there is a function selectRow which is used to select or deselect a row in the table view.

  1. class ExportViewController:
  2.  BaseTableViewController // this is my base class on UITableViewController
  3. {
  4.  private var selHelp[er = SelHelper()
  5.  private func selectRow(
  6.    select: Bool,
  7.    tableView: UITableView,
  8.    at indexPath: IndexPath)
  9.  {
  10.    if select {
  11.      tableView.selectRow(
  12.        at: indexPath,
  13.        animated: true,
  14.        scrollPosition: .none)
  15.      } else {
  16.        tableView.deselectRow(
  17.          at: indexPath,
  18.          animated: true)
  19.      }
  20.  }
  21.  ...
  22. }

Here is how I implement multiple selection in tableview events:

  1. class ExportViewController:
  2.  BaseTableViewController // this is my base class on UITableViewController
  3. {
  4.  ...
  5.  func tableView(
  6.    _ tableView: UITableView,
  7.    didDeselectRowAt indexPath: IndexPath)
  8.  {
  9.    selHelper.remove(indexPath: indexPath)
  10.    tableView.reloadRows(at: [indexPath], with: .none)
  11.  }
  12.  ...
  13.  func tableView(
  14.    _ tableView: UITableView,
  15.    didSelectRowAt indexPath: IndexPath)
  16.  {
  17.    selHelper.add(indexPath: indexPath)
  18.    tableView.reloadRows(at: [indexPath], with: .none)
  19.  }
  20.  func tableView(
  21.    _ tableView: UITableView,
  22.    cellForRowAt indexPath: IndexPath)
  23.    -> UITableViewCell // cellForRow event
  24.  {
  25.    let cell = tableView.dequeueReusableCell(
  26.          withIdentifier: "cell")
  27.    ...
  28.    let isSelected =  selHelper.isIndexPathSelected(indexPath)
  29.    cell.isSelected = isSelected
  30.    selectRow(
  31.      select: isSelected,
  32.      tableView: tableView,
  33.      at: indexPath)
  34.    return cell
  35. }

The above codes work perfectly for multiple selections.

Users may edit objects in the "edit" tab view after they make some selections in the "export" view. As mentioned above, in the case of any objects changed, I want to clear all the selections in the "export" tab view. The following codes are added in the event of viewWillAppear to clear all the catched indexPath values.

  1. class ExportViewController:
  2.  BaseTableViewController // this is my base class on UITableViewController
  3. {
  4.  override open func viewWillAppear(
  5.    _ animated: Bool)
  6.  {
  7.    super.viewWillAppear(animated)
  8.    ...
  9.    if myDBModel.dataChanged
  10.    {
  11.      selHelper.clear() // clear all indexPath
  12.    }
  13.  }
  14.  ...
  15. }

The logic is very clear. In the event of viewWillAppear, if the datasource has any changes, I'll clear all the catched selection indexPath values. This clearance will guarantee the cellForRow call to set cells unselected. Since I use a core data source for my export tableview, the cells with objects changed and in the visible area will be called to reload the cell.

Problem for visible cells

However I found a problem. If the visible cells in the tableView have any selections, the selection marks are gone. I verified that the cellForRow event was called after the event of viewWillAppear and all the selected cells are set to unselected. There is a problem: the cells are still in a high-light status. This status causes the cell to not be selected or unselected again!

For example, here is a cell is selected:

I changed the datasource first, and then back to the export tabview. The viewWillAppear event is called and all selections are set to deselected. The visible cell looks like this: the selection check mark is gone, but it looks like it is still highlighted and cannot be selected again.

I could scroll the tableView to move those visible cells to an un-visible area and scroll back. They are back to their normal status: unselected and not hilighted. However, what if there are only a small number of objects and they cannot be moved out of the tableView? The user would be stuck with this frastration situation and not be able to make a selection again. The user has to kill the app and restart the app to get back to normal.

Found a Soltion!

I think that this situation might be a problem or bug in UIKit. I have been struggling to find a way to resolve the issue for days. After several days and various attempts, suddenly, one evening very late, I found a solution to resolve the issue!

The solution is actually very simple. What I need to do is to call my private function selectRow to clear all selection for the visible cells in the event of viewWillDisappear! This clearance is OK. If there were no data changes, the cellForRow would not be called. If there is any data change, the cached selection indexPath values are cleared in the event of viewWillAppear first, and then cellForRow will be called to deselect visible cells.

  1. class ExportViewController:
  2.  BaseTableViewController // this is my base class on UITableViewController
  3. {
  4.  ...
  5.  override func viewWillDisappear(_ animated: Bool)
  6.  {
  7.    super.viewWillDisappear(animated)
  8.    guard let ips = tableViewInBase.indexPathsForSelectedRows
  9.      else { return }
  10.    // deselect all select rows in tableview on exit
  11.    // this will guaranty selection works again on
  12.    // appearance!
  13.    for ip in ips {
  14.      selectRow(select: false,
  15.        tableView: tableViewInBase,
  16.        at: ip)
  17.      }
  18.    }
  19.  ...
  20. }