Search code examples
iosswiftassertcollapsableuitableviewsectionheader

Collapsable Sections: [Assert] Unable to determine new global row index for preReloadFirstVisibleRow (0)


I'm implementing collapsable section headers in a UITableViewController.

Here's how I determine how many rows to show per section:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
    return self.sections[section].isCollapsed ? 0 : self.sections[section].items.count
}

There is a struct that holds the section info with a bool for 'isCollapsed'.

Here's how I'm toggling their states:

private func getSectionsNeedReload(_ section: Int) -> [Int]
{
    var sectionsToReload: [Int] = [section]

    let toggleSelectedSection = !sections[section].isCollapsed

    // Toggle collapse
    self.sections[section].isCollapsed = toggleSelectedSection

    if self.previouslyOpenSection != -1 && section != self.previouslyOpenSection
    {
        self.sections[self.previouslyOpenSection].isCollapsed = !self.sections[self.previouslyOpenSection].isCollapsed
        sectionsToReload.append(self.previouslyOpenSection)
        self.previouslyOpenSection = section
    }
    else if section == self.previouslyOpenSection
    {
        self.previouslyOpenSection = -1
    }
    else
    {
        self.previouslyOpenSection = section
    }

    return sectionsToReload
}



internal func toggleSection(_ header: CollapsibleTableViewHeader, section: Int)
{
    let sectionsNeedReload = getSectionsNeedReload(section)

    self.tableView.beginUpdates()
    self.tableView.reloadSections(IndexSet(sectionsNeedReload), with: .automatic)
    self.tableView.endUpdates()
}

Everything is working and animating nicely, however in the console when collapsing an expanded section, I get this [Assert]:

[Assert] Unable to determine new global row index for preReloadFirstVisibleRow (0)

This happens, regardless of whether it's the same opened Section, closing (collapsing), or if I'm opening another section and 'auto-closing' the previously open section.

I'm not doing anything with the data; that's persistent.

Could anyone help explain what's missing? Thanks


Solution

  • In order for a tableView to know where it is while it's reloading rows etc, it tries to find an "anchor row" which it uses as a reference. This is called a preReloadFirstVisibleRow. Since this tableView might not have any visible rows at some point because of all the sections being collapsed, the tableView will get confused as it can't find an anchor. It will then reset to the top.

    Solution: Add a 0 height row to every group which is collapsed. That way, even if a section is collapsed, there's a still a row present (albeit of 0px height). The tableView then always has something to hook onto as a reference. You will see this in effect by the addition of a row in numberOfRowsInSection if the rowcount is 0 and handling any further indexPath.row calls by making sure to return the phatom cell value before indexPath.row is needed if the datasource.visibleRows is 0.

    It's easier to demo in code:

    func numberOfSections(in tableView: UITableView) -> Int {
        return datasource.count
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return datasource[section].visibleRows.count == 0 ? 1 : datasource[section].visibleRows.count
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        datasource[section].section = section
        return datasource[section]
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if datasource[indexPath.section].visibleRows.count == 0 { return 0 }
        return datasource[indexPath.section].visibleRows[indexPath.row].bounds.height
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if datasource[indexPath.section].visibleRows.count == 0 { return UITableViewCell() }
    
        // I've left this stuff here to show the real contents of a cell - note how
        // the phantom cell was returned before this point.
    
        let section = datasource[indexPath.section]
        let cell = TTSContentCell(withView: section.visibleRows[indexPath.row])
        cell.accessibilityLabel = "cell_\(indexPath.section)_\(indexPath.row)"
        cell.accessibilityIdentifier = "cell_\(indexPath.section)_\(indexPath.row)"
        cell.showsReorderControl = true
        return cell
    }