/ iOS

Jerky Scrolling with UIRefreshControl + UITableView

While recording an episode for NSScreencast in which I reload rows of a UITableView while scrolling, I noticed that the scrolling acceleration would screech to a halt.

The problem, as it turned out to be, was due to UIRefreshControl's endRefreshing() method. This was not obvious at first, because this control was not always animating.

The code originally looked like this:

// update data with data from the API
self.beers = ...

// this may be animating, because the user pulled to refresh...
self.refreshControl?.endRefreshing()

if fullRefreshRequested {
    // reload everything
    self.tableView.reloadData()
} else {
    // do a smart update based on the visible index paths that changed
    let startIndex = self.beers.count - page.data.count
    let endIndex = startIndex + page.data.count - 1
    let newIndexPaths = (startIndex...endIndex).map { i in
        return IndexPath(row: i, section: 0)
    }
    let visibleIndexPaths = Set(
        self.tableView.indexPathsForVisibleRows ?? [])
    let indexPathsNeedingReload = Set(newIndexPaths)
        .intersection(visibleIndexPaths)
    self.tableView.reloadRows(at: Array(indexPathsNeedingReload), 
                            with: .fade)
}

Since the refresh control was only used for the very first load (or when the user pulled to refresh), this call seemed harmless. But as it turns out, asking the refresh control to stop animating causes the table view to stop scrolling entirely, a behavior that is quite obnoxious to the feel of the app.

The fix was simple here, by performing this only when it was really animating:

if fullRefreshRequested {
    self.refreshControl?.endRefreshing()
    self.tableView.reloadData()

With this tiny change everything continues to scroll smoothly, even when reloading rows.