Search code examples
iosswiftxcodeuitableviewautolayout

Trouble Expanding UITableView


Goal & Background

I am trying to create an expandable UITableView using this tutorial. However, I want the table to update its container's height so that the new height matches the content. The problem is this creates a visual glitch on the last header (section) in the table— but only on the first time the animation is performed.

This is what it looks like: link

My thought is that as the table expands the hidden cells, the last row is pushed out of view. So when I update the height of the view, it has to redraw the last cell (notice the color change as it's reloaded). I'm not sure where the strange slide-in animation comes from though.


Question

How would I remove this glitch or better accomplish this task?


Code

Here is my hierarchy:

+-- ParentVC
|   +-- ParentView
|   |   +-- CustomTableVC's View
|   |   |   +-- Custom UITable

(CustomTableVC is a child of ParentVC)

This is how I reload the tapped section and set the new height

// === CustomTableVC === //
func toggleSection(_ header: PTTableHeader, section: Int) {

       ...

       // Reload the section with a drop-down animation
       table.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic)

       // Update the height of the container view
       preferredContentSize.height = table.contentSize.height
}

// Height for section headers and rows: 44 (including estimated)

And here is how the parent is updated:

// === ParentVC === //
override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) {
       super.preferredContentSizeDidChange(forChildContentContainer: container)
       if let child = container as? PTTable {
              customTableVC.view.layoutIfNeeded()
              customTableViewHeightAnchor.constant = child.preferredContentSize.height
              UIView.animate(withDuration: 3, animations: {
                     view.layoutIfNeeded()
              })
       }
       // Height anchor starts at parentView's height / 3 because 
       //  I'm not sure how to make it match the table's contentSize from the get-go
}

Removing view.layoutIfNeeded() causes the last section to not perform the slide-in animation but still glitch out.


Running on iPhone 11 Pro (simulator).


Solution

  • Reflection

    I got it to work. The trick was actually pretty simple once I figured it out. I still think it's a little convoluted/smelly, but it works for my purposes and with the method I used to begin with.


    Solution

    Basically, I set the preferredContentSize before I reload the sections. This alerts the ParentVC to start animating before anything actually changes. This means that the table now has space to move the bottom section into without having to reload it.


    Code

    // === CustomTableVC === //
    func toggleSection(_ header: PTTableHeader, section: Int) {
    
           ...
    
           // Predict the height of the table BEFORE reloading it
           predictHeight(section: section, willExpand: isExpanding) // Change isExpanding with whatever Bool is tracking the expand/collapse state of the section
    
           // THEN reload the section with a drop-down animation
           table.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic)
    
           // Optionally do the above with an animation using UIView.animate() or UIView.transition()
    
           // And FINALLY update the height of the container view
           // This one is probably optional, but it will be more exact depending on what methods you use to predict the height in predictHeight()
           preferredContentSize.height = table.contentSize.height
    }
    
    func predictHeight(section: Int, willExpand: Bool) {
            // Get the heights of all the known headers/footers/rows
            let tableSectionsHeight = CGFloat(table.numberOfSections) * (table.estimatedSectionHeaderHeight + table.sectionFooterHeight)
            let tableCellsHeight = CGFloat(table.visibleCells.count) * table.estimatedRowHeight
    
            // Calculate the height of the section being expanded/collapsed
            // With the method I used, I can't just do table.numberOfRows(inSection: Int) since expanding/collapsing is essentially just adding/removing those rows
            // Instead I need to store a reference to the number of rows per section and access it via that array, object, etc.
            let sectionContentHeight = willExpand ? CGFloat(rowCounts[section]) * table.estimatedRowHeight : 0 // 0 if collapsing
    
            // Set the preferredContentSize so that the ParentVC picks it up 
            preferredContentSize.height = tableSectionsHeight + tableCellsHeight + sectionContentHeight
        }