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

52,858

Solution 1

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

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

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

@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

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()

Share:
52,858

Related videos on Youtube

David
Author by

David

Updated on August 04, 2021

Comments

  • David
    David over 2 years

    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.

    • Lyndsey Scott
      Lyndsey Scott about 9 years
      A couple things... (1) Yes you can definitely reload certain rows using reloadRowsAtIndexPaths. But (2) what do you mean by "jumpy" and (3) have you set an estimated row height? (Just trying to figure out if there's a better solution that would allow you to update the table dynamically.)
    • David
      David about 9 years
      @LyndseyScott, yes, I have set an estimated row height. By jumpy I mean that as I scroll up, the rows are shifting upwards. I believe this is because I set an estimated row height of 128, and then as I scroll up, all my posts above in the UITableView are smaller, so it shrinks the height, causing my table to jump. I'm thinking of doing reloadRowsAtIndexPaths from row x to the last row in my TableView... but because I'm inserting new rows, it won't work, I can't know what the end of my tableview will be before I reloaded the data.
    • Hot Licks
      Hot Licks about 9 years
      how do I only reloadData from a certain IndexPath to the end of the UITableView? -- You do that by reading the documentation.
    • Lyndsey Scott
      Lyndsey Scott about 9 years
      Note for future answer seekers: It seems as if the issue comes from using tableview.rowHeight = UITableViewAutomaticDimension. The issue was also mentioned in this answer: stackoverflow.com/q/25999880/2274694
    • rad
      rad about 9 years
      @LyndseyScott still i can't solve problem, is there any good solution?
    • user3344977
      user3344977 about 9 years
      Did you ever find a solution for this problem? I am experiencing the exact same problem as seen in your video.
    • z22
      z22 over 8 years
      Did anyone find the solution to this issue? I am too facing this issue shown in the video
    • Jonny
      Jonny over 6 years
      I was using exact cell heights but was supposedly hit by this, or a very similar problem. Setting the exact size as estimated size (as suggested in several answers, but I could do it directly in my storyboard) fixed that problem - the table view would jump around a bit strangely after deleting some source data and then doing reloadData().
    • Srujan Simha
      Srujan Simha almost 6 years
      None of the answers below worked for me.
    • tboyce12
      tboyce12 over 5 years
      Can you please re-upload the video? Link appears broken.
    • mfaani
      mfaani over 5 years
      There seems to be a bug for when you use UITableViewAutomaticDimension . See this answer and other answers to the question. @SrujanSimha
  • David
    David about 9 years
    I've tried the beginUpdates/endUpdates method, but that only affects the visible rows of my table. I still have the issue when I scroll up.
  • Lyndsey Scott
    Lyndsey Scott about 9 years
    @David Probably because you're using estimated row heights.
  • David
    David about 9 years
    Should I get rid of my EstimatedRowHeights, and instead replace it with the beginUpdates and endUpdates?
  • Lyndsey Scott
    Lyndsey Scott about 9 years
    @David You wouldn't be "replacing" anything, but it really depends on the desired behavior... If you want to use estimated rows height and just reload the indexes below the current visible portion of the table, you can do that like I said using reloadRowsAtIndexPaths
  • David
    David about 9 years
    One of my issues with trying the reladRowsAtIndexPaths method is that I'm implementing infinite scrolling, so when I'm reloadingData it is becauseI just added 15 more rows to the dataSource. This means that the indexPaths for those rows don't yet exist in the UITableView
  • Lyndsey Scott
    Lyndsey Scott about 9 years
    @David Oh well if you're inserting rows, why not use insertRowsAtIndexPaths: instead
  • David
    David about 9 years
    Interesting. I suppose I'm too used to Android Dev where you just notify the list of changes and it does the rest. I'm still a bit perplexed with how I would go about inserting rows to the end of the table. Would I have to create 15 indexPaths and give each their own row number if I'm going to insert 15 items to the end of the UITableView?
  • Lyndsey Scott
    Lyndsey Scott about 9 years
    @David Yeah, you could create an array of the 15 index paths.
  • David
    David about 9 years
    I was under the impression IndexPaths were for the UITableView to handle and fetch from, not to create on ones own. I see. So even if my UITableView currently only had 30 items, and I'm appending 15, I'd use indexPaths with row 30 - 44 (since its 0-indexed)?
  • Lyndsey Scott
    Lyndsey Scott about 9 years
    @David I'd recommend trying it yourself and seeing if it work for your particular case.
  • David
    David about 9 years
    I'm using the insert method, and the rows are being properly inserted, but the problem still persists and for some reason the heights for the rows not above the screen (not visible) are still being recalculated and thus causing the jumpy behavior
  • Lyndsey Scott
    Lyndsey Scott about 9 years
    @David And, just curious, if you remove the estimated row height does the issue persist?
  • David
    David about 9 years
    Yeah, it still persists. I included a video in the Question above so you can visually see the issue.
  • Lyndsey Scott
    Lyndsey Scott about 9 years
    @David Oh, I see. It looks like your table height of the elements changes below the visible table thus shrinking the content size of the scrollview.
  • David
    David about 9 years
    If it changes below the visible table, wouldn't it just change once and I'd see one bump. Instead, it seems like it continues to be changed (if that is the case)
  • Lyndsey Scott
    Lyndsey Scott about 9 years
    @David, depends on how often you're reloading/inserting?
  • Lyndsey Scott
    Lyndsey Scott about 9 years
  • Flappy
    Flappy almost 8 years
    Fixed it wen overriding the estimatedHeightForRowAtIndexPath method with an high value, for example 300f
  • Artem Z.
    Artem Z. over 7 years
    Thanks, u really save my day :) Works in objc too
  • Gerharbo
    Gerharbo over 7 years
    Don't forget to initialize cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
  • Natalia
    Natalia about 7 years
    Thanks this worked great! in fact I was able to remove my implementation of func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, this handles all the height calculation I need.
  • liushuaikobe
    liushuaikobe almost 7 years
    estimatedHeightForRowAtIndexPath: returns a double value may cause a *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:] error. To fix it, return floorf(height.floatValue); instead.
  • Madhuri
    Madhuri almost 7 years
    Hi @lgor, I'm having the same issue & trying to implement your solution. The issue i'm getting is estimatedHeightForRowAtIndexPath gets called prior to willDisplayCell, so cell's height is not calculated when estimatedHeightForRowAtIndexPath is called. Any Help?
  • Donnit
    Donnit almost 7 years
    @Madhuri effective heights should be calculated in "heightForRowAtIndexPath", that is called for every cell on the screen just before willDisplayCell, which will set the height in the dictionary for later use in estimatedRowHeight (on table reload).
  • Igor
    Igor almost 7 years
    @Madhuri, this solution for "Dynamic cell heights". First time height of cell defined by autolayout. Check your constraints and set tableView.rowHeight = UITableViewAutomaticDimension;
  • MJQZ1347
    MJQZ1347 over 6 years
    After struggling many hours with persistant jumping I figured out that I forgot adding UITableViewDelegate to my class. Conforming to that protocol is neccessary because it contains the above shown willDisplay function. I hope I can save someone the same struggle.
  • Alexey Chekanov
    Alexey Chekanov almost 6 years
    How to deal with row insertion/deletion using this solution? TableView jumps, as the dictionary data isn't actual.
  • Louis de Decker
    Louis de Decker almost 6 years
    thank you, it's exactly why my tableView was so jumpy.
  • tryKuldeepTanwar
    tryKuldeepTanwar almost 6 years
    I've always underestimated estimatedHeightForRowAtIndexPath before the day i shaw you answer... wow man works like a charm.
  • Gaganpreet
    Gaganpreet over 5 years
    Hello @Igor i'm having same tableview jumping problem after reload every time, i'm trying many solutions to fix it but same problem is there any solution.
  • mfaani
    mfaani over 5 years
    How should you deal with row insertion/deletion using this solution? TableView jumps, as the dictionary data isn't actual.
  • matt
    matt over 5 years
    How does this do any reloading? You call it reloadWithoutAnimation but where's the reload part?
  • rgreso
    rgreso over 5 years
    For me this worked with combination of setting estimatedRowHeight, estimatedSectionHeaderHeight, estimatedSectionFooterHeight to 1.
  • Srujan Simha
    Srujan Simha over 5 years
    @matt you could call tableView.reloadData() first and then tableView.reloadWithoutAnimation(), it still works.
  • Vitalii
    Vitalii over 5 years
    An old answer, but it is still actual as of 2018. Unlike all other answers, this one suggests setting estimatedRowHeigh once in viewDidLoad, which helps when cells are of same or very similar height. Thanx. BTW, alternatively esimatedRowHeight can be set via Interface Builder in Size Inspector > Table View > Estimate.
  • T.Y. Kucuk
    T.Y. Kucuk over 5 years
    Great! None of above didn't work for me neither. Even all heights and estimated heights are totally the same. Interesting.
  • Ning
    Ning over 5 years
    works great! especially on the last cell when reload row.
  • Trev14
    Trev14 about 5 years
    Thank you for the Swift answer. In my case I was having some SUPER weird behavior of cells going out of order on reload when the table view was scrolled to/near the bottom. I'll be using this from now on whenever I have self-sizing cells.
  • Kakashi
    Kakashi about 5 years
    Don't work for me. It is crash at tableView.endUpdates(). Can someone help me!
  • Rohan Sanap
    Rohan Sanap about 5 years
    @Flappy it is interesting how solution provided by you works and is shorter than other suggested techniques. Do consider posting it as an answer.
  • Adam S.
    Adam S. almost 5 years
    Works perfectly in Swift 4.2
  • BennyTheNerd
    BennyTheNerd almost 5 years
    The key for me was implementing his UITableView extension here. Very clever. Thanks rastislv
  • Soufian Hossam
    Soufian Hossam almost 5 years
    Works perfectly but it has only one drawback, you lose the animation when inserting header, footer or row.
  • Luke Irvin
    Luke Irvin almost 5 years
    Where would reloadSectionWithouAnimation be called? So for example, users can post an image in my app (like Instagram); I can get the images to resize, but in most cases I have to scroll the table cell off scree for that to happen. I want the cell to be the correct size once the table goes through reloadData.
  • henrique
    henrique almost 5 years
    Works like a charm. Do you have any idea why this happens? I'd suggest that is due to the UITableView algorithm used to find the height for cell given an estimated height "considerably far" from the correct/desired/necessary height.
  • Igor
    Igor almost 5 years
    yeah, I guess the same way
  • Philip Borbon
    Philip Borbon over 4 years
    A life saver. So helpful when trying to add more items in the datasource. Prevents jumping of newly added cells to the center of the screen.
  • nteissler
    nteissler over 4 years
    provided a more accurate estimated height helped me. I also had a multi-section grouped table view style, and had to implement tableView(_:estimatedHeightForHeaderInSection:)
  • Zack
    Zack about 4 years
    This is a great answer - my only suggestion would be to replace your default value in the estimatedHeightForRowAt: method with UITableView.automaticDimension. This way it will fallback to the (often imprecise but hopefully close) automatically determined value from Apple rather than 70.
  • Raj D
    Raj D almost 4 years
    WOW. Such little concept can made a huge difference! I wonder if there's something for collectionview too
  • Legonaftik
    Legonaftik over 3 years
    What if your screen has, let's say, pull-to-refresh and you got new elements inserted at the top? At this moment [IndexPath: CGFloat] would be invalidated.
  • Yahia
    Yahia over 3 years
    That worked for me, but the question is Why?
  • ekashking
    ekashking almost 2 years
    That actually works. However, the question now is: how come estimatedHeightForRowAtIndexPath but the regular heightForRowAtIndexPath doesn't???
  • Igor
    Igor almost 2 years
    with dynamic cell height estimatedHeightForRowAtIndexPath used for calculate cells size on scrolling