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

1

11

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

Mascon answered 3/10, 2019 at 19:29 Comment(7)
Is your tableview made up of a bunch of sections and not many actual rows?Masefield
Did you ever manage to fix this?Cateran
@ByronCoetsee Yes, until a section is expanded. So when all collapsed it's just section headers. When one is expanded it's all section headers for the non-expanded sections and a section header and then cells for data.Mascon
@PaulDoesDev I did, but not by using this mechanism. I completely rewrote it so that whilst it appears the same, it works completely differently. However I'm going to leave this here in case someone can elegantly fix this, or it helps others in some way.Mascon
I managed to solve it by adding "phantom" rows under each collapsed section header... row height of 0. Works a treat :)Masefield
@ByronCoetsee Ha! I considered that but it felt... 'dirty'... lol. If you post a copy of your code and demonstrate the fix, I'll mark it as the answer. I just wish there was a cleaner way.Mascon
@Mascon haha yeah I thought it may feel like a hack and it technically is, but the amount of code and the fact that it's actually pretty clean means I can let myself sleep at night :P code posted belowMasefield
M
14

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
}
Masefield answered 22/11, 2019 at 12:57 Comment(2)
Hey @Byron Coetsee, your explanation made a lot of sense but it didn't solve the assert issue for me.Seagirt
It did not solve my issue either, my cells become "un-deselectable" after selecting a cell, collapsing the section, then expanding the sectionElaineelam

© 2022 - 2024 — McMap. All rights reserved.