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

Pre-Scrolling a Table to a specific cell at load

Although that’s something that appears fairly easy, the truth -for me at least-, is that scrolling a UITableView to a specific cell every time you load it may prove tricky. 

In the app I’m working on I implement it in the following way: every time a view appeared I assess if the top visible row is the one it should. If it is, no action is taken, else we’ll scroll to the right row. 

    override func viewDidAppear(_ animated: Bool) {

        super.viewDidAppear(animated)

        //Check if we need to scroll to a specific cell

        if getTopVisibleRow() != getTableLastViewedTopPosition() {

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

        }

 

    }

For reference here’s what I’m doing in the getTopVisibleRow(), note that is is important only if you’re using a translucid navbar:

    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

 

    }

For getTableLastViewedTopPosition() I’m simply fetching from the model what cell index should we be showing. 

Questions / comments? I’m at @MarcMasVi 

Marc

Inserting cells at the top of a UITableView with no scrolling

If you would like your table to be refreshed “ala Twitter”, with new data being added on top, here’s a quick an easy way to do it for iOS: 

func updateTableViewAfterAdditionInNewArray(){

        //Get new feeds and check if counts are different

        let countOfAddedItems = oldArray.count – newArray.count

        

        //If we’re adding information

        if (oldArray) > 0 {

            if debugMode == true{

                print(“We have a difference in counts of \(oldArray)”)

            }

            

            if oldArray.count != 0 {

                //Update data model

                oldArray = newArray

                

                var initialContentOffSet = tableView.contentOffset.y

                //If offset is less than 0 due to refresh up gesture, assume 0

                if initialContentOffSet < 0 {

                    initialContentOffSet = 0

                }

                //Reload, scroll and offset

                tableView.reloadData()

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

                tableView.contentOffset.y = tableView.contentOffset.y  + initialContentOffSet

            }else{

                if debugMode == true{

                    print(“There was no data in oldArray, avoid scrolling. “)

                }

                //Update data model

                oldArray = newArray

                

                tableView.reloadData()

            }

            

        }

 

    }

Hope it helps! I’m really enjoying going back to iOS development, expect more iOS posts this next couple of months as I’m ramping up on my next project. 

Questions / comments / suggestions? I’m at @MarcMasVi 

 

Marc