Uitableview Is Jumping When I Insert New Rows

Why does my UITableView jump when inserting or removing a row?

On iOS 11, UITableView uses estimated row height as default.

It leads to unpredictable behaviors when inserting/reloading or deleting rows because the UITableView has a wrong content size most of the time:

To avoid too many layout calculations, the tableView asks heightForRow only for each cellForRow call and remembers it (in normal mode, the tableView asks heightForRow for all the indexPaths of the tableView). The rest of the cells has a height equal to the estimatedRowHeight value until their corresponding cellForRow is called .

// estimatedRowHeight mode
contentSize.height = numberOfRowsNotYetOnScreen * estimatedRowHeight + numberOfRowsDisplayedAtLeastOnce * heightOfRow

// normal mode
contentSize.height = heightOfRow * numberOfCells

I guess UIKit struggles to animate correctly the changes because of this trick.

One solution is to disable the estimatedRowHeight mode by setting estimatedRowHeight to 0 and implementing heightForRow for each of your cells.

Of course, if your cells have dynamic heights (with onerous layout calculations most of time so you used estimatedRowHeight for a good reason), you would have to find a way to reproduce the estimatedRowHeight optimization without compromising the contentSize of your tableView. Take a look at AsyncDisplayKit or UITableView-FDTemplateLayoutCell.

Another solution is to try to find a estimatedRowHeight which suits well. Since iOS 10, you can also try to use UITableView.automaticDimension. UIKit will find a value for you:

tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = UITableView.automaticDimension

On iOS 11, it's already the default value.

UITableView changes position when inserting new rows

I suspect the cause of this is actually to do with using

tableView.rowHeight = UITableView.automaticDimension
....
tableView.estimatedRowHeight = 200

The position changes as the offset is changing when new cells are inserted.

I would start by keeping some sort of cache containing your cell heights

private var sizeCache: [IndexPath: CGFloat] = [IndexPath: CGFloat]()

You can then capture that as the cell is scrolled off screen

func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
sizeCache[indexPath] = cell.frame.size.height
}

Now make sure to apply that size from the cache

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return sizeCache[indexPath] ?? UITableView.automaticDimension
}

Now as cells are inserted and they jump with the new offset, they should render with their correct height, meaning the view should essentially stay on position.

UITableView jumps and flickers when scrolling

To remove the jumping issue you need to set estimatedHeightForRowAt the same as your row height. Assuming you will have no performance issues you can simply do the following:

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.tableView(tableView, heightForRowAt: indexPath)
}

Or if the cell height is constant you can do tableView.estimatedRowHeight = 70.0.

Why this happens is because table view when reloading will use estimatedRowHeight for the cells that are invisible which results in jumping when the estimated height differs from the actual. To give you an idea:

Let's say that estimated height is 50 while the real height is 75. Now that you have scrolled down so that 10 cells are off the screen you have 10*75 = 750 pixels of content offset. No when reload occurs table view will ignore how many cells are hidden and will try to recompute that. It will keep reusing estimated row height until it finds the index path that should be visible. In this example it starts calling your estimatedHeightForRow with indexes [0, 1, 2... and increasing the offset by 50 until it gets to your content offset which is still 750. So that means it gets to index 750/50 = 15. And this produces a jump from cell 10 to cell 15 on reload.

As for the flickering there are many possibilities. You could avoid reloading the cells that don't need reloading by reloading only the portion of data source that has changed. In your case that means inserting new rows like:

tableView.beginUpdates()
tableView.insertRows(at: myPaths, with: .none)
tableView.endUpdates()

Still it seems strange you even see flickering. If only image flickers then the issue may be elsewhere. Getting an image like this is usually an asynchronous operation, even if the image is already cached. You could avoid it by checking if you really need to update the resource. If your cell is already displaying the image you are trying to show then there is no reason to apply the new resource:

if let url = BrandsManager.brandLogoURL(forLogoName: brand.logo!) {
if url != cell.currentLeftImageURL { // Check if new image needs to be applied
let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
cell.currentLeftImageURL = url // Save the new URL
cell.leftImageView.kf.setImage(with: resource)
}
} else {
print("Cannot form url for brand logo")
}

I would rather put this code into the cell itself though

var leftImageURL: URL {
didSet {
if(oldValue != leftImageURL) {
let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
leftImageView.kf.setImage(with: resource)
}
}
}

but this is completely up to you.



Related Topics



Leave a reply



Submit