reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling

IosUitableviewSwiftAutolayout

Ios Problem Overview


I feel like this might be a common issue and was wondering if there was any common solution to it.

Basically, my UITableView has dynamic cell heights for every cell. If I am not at the top of the UITableView and I tableView.reloadData(), scrolling up becomes jumpy.

I believe this is due to the fact that because I reloaded data, as I'm scrolling up, the UITableView is recalculating the height for each cell coming into visibility. How do I mitigate that, or how do I only reloadData from a certain IndexPath to the end of the UITableView?

Further, when I do manage to scroll all the way to the top, I can scroll back down and then up, no problem with no jumping. This is most likely because the UITableViewCell heights were already calculated.

Ios Solutions


Solution 1 - Ios

To prevent jumping you should save heights of cells when they loads and give exact value in tableView:estimatedHeightForRowAtIndexPath:

Swift:

var cellHeights = [IndexPath: CGFloat]()

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

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

Objective C:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = @{}.mutableCopy;

// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;

// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    [cellHeightsDictionary setObject:@(cell.frame.size.height) forKey:indexPath];
}

// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
    if (height) return height.doubleValue;
    return UITableViewAutomaticDimension;
}

Solution 2 - Ios

Swift 3 version of accepted answer.

var cellHeights: [IndexPath : CGFloat] = [:]


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

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

Solution 3 - Ios

The jump is because of a bad estimated height. The more the estimatedRowHeight differs from the actual height the more the table may jump when it is reloaded especially the further down it has been scrolled. This is because the table's estimated size radically differs from its actual size, forcing the table to adjust its content size and offset. So the estimated height shouldn't be a random value but close to what you think the height is going to be. I have also experienced when i set UITableViewAutomaticDimension if your cells are same type then

func viewDidLoad() {
     super.viewDidLoad()
     tableView.estimatedRowHeight = 100//close to your cell height
}

if you have variety of cells in different sections then I think the better place is

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
     //return different sizes for different cells if you need to
     return 100
}

Solution 4 - Ios

@Igor answer is working fine in this case, Swift-4 code of it.

// declaration & initialization  
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]  

in following methods of UITableViewDelegate

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  // print("Cell height: \(cell.frame.size.height)")
  self.cellHeightsDictionary[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
  if let height =  self.cellHeightsDictionary[indexPath] {
    return height
  }
  return UITableView.automaticDimension
}

Solution 5 - Ios

I have tried all the workarounds above, but nothing worked.

After spending hours and going through all the possible frustrations, figured out a way to fix this. This solution is a life savior! Worked like a charm!

Swift 4

let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)

I added it as an extension, to make the code look cleaner and avoid writing all these lines every time I want to reload.

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        beginUpdates()
        endUpdates()
        layer.removeAllAnimations()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

finally ..

tableView.reloadWithoutAnimation()

OR you could actually add these line in your UITableViewCell awakeFromNib() method

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale

and do normal reloadData()

Solution 6 - Ios

I use more ways how to fix it:

For view controller:

var cellHeights: [IndexPath : CGFloat] = [:]


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

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

as the extension for UITableView

extension UITableView {
  func reloadSectionWithoutAnimation(section: Int) {
      UIView.performWithoutAnimation {
          let offset = self.contentOffset
          self.reloadSections(IndexSet(integer: section), with: .none)
          self.contentOffset = offset
      }
  }
}

The result is

tableView.reloadSectionWithoutAnimation(section: indexPath.section)

Solution 7 - Ios

I ran into this today and observed:

  1. It's iOS 8 only, indeed.
  2. Overridding cellForRowAtIndexPath doesn't help.

The fix was actually pretty simple:

Override estimatedHeightForRowAtIndexPath and make sure it returns the correct values.

With this, all weird jittering and jumping around in my UITableViews has stopped.

NOTE: I actually know the size of my cells. There are only two possible values. If your cells are truly variable-sized, then you might want to cache the cell.bounds.size.height from tableView:willDisplayCell:forRowAtIndexPath:

Solution 8 - Ios

You can in fact reload only certain rows by using reloadRowsAtIndexPaths, ex:

tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)

But, in general, you could also animate table cell height changes like so:

tableView.beginUpdates()
tableView.endUpdates()

Solution 9 - Ios

Overriding the estimatedHeightForRowAtIndexPath method with an high value, for example 300f

This should fix the problem :)

Solution 10 - Ios

Here's a bit shorter version:

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

Solution 11 - Ios

There is a bug which I believe was introduced in iOS11.

That is when you do a reload the tableView contentOffSet gets unexpectedly altered. In fact contentOffset should not change after a reload. It tends to happen due to miscalculations of UITableViewAutomaticDimension

You have to save your contentOffSet and set it back to your saved value after your reload is finished.

func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){
    
    DispatchQueue.main.async { [weak self] () in
        
        self?.tableView.reloadData()
        self?.tableView.layoutIfNeeded()
        self?.tableView.contentOffset = offset
    }
}

How you use it?

someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)

This answer was derived from here

Solution 12 - Ios

This one worked for me in Swift4:

extension UITableView {
    
    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        reloadData()
        layoutIfNeeded()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

Solution 13 - Ios

One of the approach to solve this problem that I found is

CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
   UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()

Solution 14 - Ios

None of these solutions worked for me. Here's what I did with Swift 4 & Xcode 10.1...

In viewDidLoad(), declare table dynamic row height and create correct constraints in cells...

tableView.rowHeight = UITableView.automaticDimension

Also in viewDidLoad(), register all your tableView cell nibs to tableview like this:

tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")

In tableView heightForRowAt, return height equal to each cell's height at indexPath.row...

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    
    if indexPath.row == 0 {
        let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
        return cell.layer.frame.height
    } else if indexPath.row == 1 {
        let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
        return cell.layer.frame.height
    } else {
        let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
        return cell.layer.frame.height
    } 
    
}

Now give an estimated row height for each cell in tableView estimatedHeightForRowAt. Be accurate as you can...

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {

    if indexPath.row == 0 {
        return 400 // or whatever YourTableViewCell's height is
    } else if indexPath.row == 1 {
        return 231 // or whatever YourSecondTableViewCell's height is
    } else {
        return 216 // or whatever YourThirdTableViewCell's height is
    } 

}

That should work...

I didn't need to save and set contentOffset when calling tableView.reloadData()

Solution 15 - Ios

I have 2 different cell heights.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
        return Helper.makeDeviceSpecificCommonSize(cellHeight)
    }

After I added estimatedHeightForRowAt, there was no more jumping.

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
    return Helper.makeDeviceSpecificCommonSize(cellHeight)
}

Solution 16 - Ios

Try to call cell.layoutSubviews() before returning cell in func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. It's known bug in iOS8.

Solution 17 - Ios

You can use the following in ViewDidLoad()

tableView.estimatedRowHeight = 0     // if have just tableViewCells <br/>

// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0

Solution 18 - Ios

I had this jumping behavior and I initially was able to mitigate it by setting the exact estimated header height (because I only had 1 possible header view), however the jumps then started to happen inside the headers specifically, not affecting the whole table anymore.

Following the answers here, I had the clue that it was related to animations, so I found that the table view was inside a stack view, and sometimes we'd call stackView.layoutIfNeeded() inside an animation block. My final solution was to make sure this call doesn't happen unless "really" needed, because layout "if needed" had visual behaviors in that context even when "not needed".

Solution 19 - Ios

I had the same issue. I had pagination and reloading data without animation but it did not help the scroll to prevent jumping. I have different size of IPhones, the scroll was not jumpy on iphone8 but it was jumpy on iphone7+

I applied following changes on viewDidLoad function:

    self.myTableView.estimatedRowHeight = 0.0
    self.myTableView.estimatedSectionFooterHeight = 0
    self.myTableView.estimatedSectionHeaderHeight = 0

and my problem solved. I hope it helps you too.

Solution 20 - Ios

For me, it worked with "heightForRowAt"

extension APICallURLSessionViewController: UITableViewDelegate {

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    print("Inside heightForRowAt")
    return 130.50
}
}

Solution 21 - Ios

For me the working solution is

UIView.setAnimationsEnabled(false)
    tableView.performBatchUpdates { [weak self] in
    self?.tableView.reloadRows(at: [indexPath], with: .none)
} completion: { [weak self] _ in
    UIView.setAnimationsEnabled(true)
    self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) // remove if you don't need to scroll
}

I have expandable cells.

Solution 22 - Ios

Actually I found if you use reloadRows causing a jump problem. Then you should try to use reloadSections like this:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDavidView Question on Stackoverflow
Solution 1 - IosIgorView Answer on Stackoverflow
Solution 2 - IosCasey WagnerView Answer on Stackoverflow
Solution 3 - IosKrishna KishoreView Answer on Stackoverflow
Solution 4 - IosKiran JasvaneeView Answer on Stackoverflow
Solution 5 - IosSrujan SimhaView Answer on Stackoverflow
Solution 6 - IosrastislvView Answer on Stackoverflow
Solution 7 - IosMarcWanView Answer on Stackoverflow
Solution 8 - IosLyndsey ScottView Answer on Stackoverflow
Solution 9 - IosFlappyView Answer on Stackoverflow
Solution 10 - Iosjake1981View Answer on Stackoverflow
Solution 11 - IosmfaaniView Answer on Stackoverflow
Solution 12 - IosDmytro BrovkinView Answer on Stackoverflow
Solution 13 - IosShaileshAherView Answer on Stackoverflow
Solution 14 - IosMichael ColonnaView Answer on Stackoverflow
Solution 15 - IossabilandView Answer on Stackoverflow
Solution 16 - IosCrimeZoneView Answer on Stackoverflow
Solution 17 - IosVidView Answer on Stackoverflow
Solution 18 - IosGobeView Answer on Stackoverflow
Solution 19 - IosBurcu KutluayView Answer on Stackoverflow
Solution 20 - IosPrachi BileView Answer on Stackoverflow
Solution 21 - IosDmitryView Answer on Stackoverflow
Solution 22 - IosMichaelView Answer on Stackoverflow