Reloading/inserting Dynamic Height cells and keeping Scroll position

For an upcoming app I had to insert Dynamic Height cells while keeping the tableView offset -scrolling position- constant. This would typically be a trivial issue, but given that when using Dynamic Height cells nor the contentSize nor the offset can be trusted it’s a bit more tricky than it seems…

What I was trying to do was adding cells at the top of my tableView without any scroll taking place, similarly to what Twitter, Tweetbot or Twitterrific apps do. 

Screen Shot 2018 12 30 at 6 17 23 PM

Green cells in the “After” scenario show how the new cells have been inserted at the top, without affecting scrolling. 

The approach that ended up working smoothly was:

Step 1. Storing in a variable the UITableViewCell below the Navigation Bar and storing the offset of that cell in relation to the Navigation Bar. 

Step 2. Insert cells / reloadData.

Step 3.Scroll to the cell we saved before the insert/reload and then add the offset. 

Here’s the code:

 

        UIView.performWithoutAnimation {

            //Step 1 Detect & Store

            let topWillBeAt = getTopVisibleRow() + countOfAddedItems

            let oldHeightDifferenceBetweenTopRowAndNavBar = heightDifferenceBetweenTopRowAndNavBar()

            

            //Step 2 Insert

            self.tableView.insertRows(at: arrayOfIndexPaths, with: .none)

            

            //Step 3 Restore Scrolling

            tableView.scrollToRow(at: IndexPath(row: topWillBeAt, section: 0), at: .top, animated: false)

            tableView.contentOffset.y = tableView.contentOffset.y – oldHeightDifferenceBetweenTopRowAndNavBar

 

        }

 

And supporting functions:

    func getTopVisibleRow () -> Int {

        //We need this to accounts for the translucency below the nav bar

        let navBar = navigationController?.navigationBar

        let whereIsNavBarInTableView = tableView.convert(navBar!.bounds, from: navBar)

        let pointWhereNavBarEnds = CGPoint(x: 0, y: whereIsNavBarInTableView.origin.y + whereIsNavBarInTableView.size.height + 1)

        let accurateIndexPath = tableView.indexPathForRow(at: pointWhereNavBarEnds)

        return accurateIndexPath?.row ?? 0

    }

    

    func heightDifferenceBetweenTopRowAndNavBar()-> CGFloat{

        let rectForTopRow = tableView.rectForRow(at:IndexPath(row:  getTopVisibleRow(), section: 0))

        let navBar = navigationController?.navigationBar

        let whereIsNavBarInTableView = tableView.convert(navBar!.bounds, from: navBar)

        let pointWhereNavBarEnds = CGPoint(x: 0, y: whereIsNavBarInTableView.origin.y + whereIsNavBarInTableView.size.height)

        let differenceBetweenTopRowAndNavBar = rectForTopRow.origin.y – pointWhereNavBarEnds.y

        return differenceBetweenTopRowAndNavBar

 

    }

 

Questions / comments / suggestions? @MarcMasVi 

PS. On another topic, if you get some jumpy scrolling with Dynamic Height cells I strongly suggest you to look into this question from Stack Overflow. 

Marc