Jerky Scrolling After Updating UITableViewCell in place with UITableViewAutomaticDimension

28,874

Solution 1

Here is the best solution I found to solve this kind of problem (scrolling problem + reloadRows + iOS 8 UITableViewAutomaticDimension);

It consists by keeping every heights in a dictionary and updating them (in the dictionary) as the tableView will display the cell.

You will then return the saved height in - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath method.

You should implement something like this :

Objective-C

- (void)viewDidLoad {
    [super viewDidLoad];

    self.heightAtIndexPath = [NSMutableDictionary new];
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];
    if(height) {
        return height.floatValue;
    } else {
        return UITableViewAutomaticDimension;
    }
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = @(cell.frame.size.height);
    [self.heightAtIndexPath setObject:height forKey:indexPath];
}

Swift 3

@IBOutlet var tableView : UITableView?
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}

Solution 2

We had the same problem. It comes from a bad estimation of the cell height that causes the SDK to force a bad height which will cause the jumping of cells when scrolling back up. Depending on how you built your cell, the best way to fix this is to implement the UITableViewDelegate method - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath

As long as your estimation is pretty close to the real value of the cell height, this will almost cancel the jumping and jerkiness. Here's how we implemented it, you'll get the logic:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // This method will get your cell identifier based on your data
    NSString *cellType = [self reuseIdentifierForIndexPath:indexPath];

    if ([cellType isEqualToString:kFirstCellIdentifier])
        return kFirstCellHeight;
    else if ([cellType isEqualToString:kSecondCellIdentifier])
        return kSecondCellHeight;
    else if ([cellType isEqualToString:kThirdCellIdentifier])
        return kThirdCellHeight;
    else {
        return UITableViewAutomaticDimension;
    }
}

Added Swift 2 support

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    // This method will get your cell identifier based on your data
    let cellType = reuseIdentifierForIndexPath(indexPath)

    if cellType == kFirstCellIdentifier 
        return kFirstCellHeight
    else if cellType == kSecondCellIdentifier
        return kSecondCellHeight
    else if cellType == kThirdCellIdentifier
        return kThirdCellHeight
    else
        return UITableViewAutomaticDimension  
}

Solution 3

dosdos answer worked for me in Swift 2

Declare the ivar

var heightAtIndexPath = NSMutableDictionary()

in func viewDidLoad()

func viewDidLoad() {
  .... your code
  self.tableView.rowHeight = UITableViewAutomaticDimension
}

Then add the following 2 methods:

override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
   let height = self.heightAtIndexPath.objectForKey(indexPath)
   if ((height) != nil) {
     return CGFloat(height!.floatValue)
   } else {
    return UITableViewAutomaticDimension
   }
 }

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
  let height = cell.frame.size.height
  self.heightAtIndexPath.setObject(height, forKey: indexPath)
}

SWIFT 3:

var heightAtIndexPath = [IndexPath: CGFloat]()

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

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

Solution 4

@dosdos solution is working fine

but there is something you should added

following @dosdos answer

Swift 3/4

@IBOutlet var tableView : UITableView!
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}

then use this lines when ever you want , for me I use it inside textDidChange

  1. first reload Tableview
  2. update constraint
  3. finally move to top Tableview

    tableView.reloadData()
    self.tableView.layoutIfNeeded()
    self.tableView.setContentOffset(CGPoint.zero, animated: true)
    

Solution 5

I was facing the same problem too. I did find a workaround, but it doesn't completely fix the jerk. But it seems to be a lot better compared to the previous choppy scrolling.

In your UITableView delegate method :cellForRowAtIndexPath:, try using the following two methods to update the constraints before returning the cell. (Swift language)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

EDIT: You may also have to play around with the tableView.estimatedRowHeight value to get a smoother scrolling.

Share:
28,874

Related videos on Youtube

Bryan Alger
Author by

Bryan Alger

Updated on May 30, 2020

Comments

  • Bryan Alger
    Bryan Alger almost 4 years

    I am building an app that has a feed view for user-submitted posts. This view has a UITableView with a custom UITableViewCell implementation. Inside this cell, I have another UITableView for displaying comments. The gist is something like this:

    Feed TableView
      PostCell
        Comments (TableView)
          CommentCell
      PostCell
        Comments (TableView)
          CommentCell
          CommentCell
          CommentCell
          CommentCell
          CommentCell
    

    The initial feed will download with 3 comments for previewing, but if there are more comments, or if the user adds or deletes a comment, I want to update the PostCell in place inside of the feed table view by adding or removing CommentCells to the comments table inside of the PostCell. I am currently using the following helper to accomplish that:

    // (PostCell.swift) Handle showing/hiding comments
    func animateAddOrDeleteComments(startRow: Int, endRow: Int, operation: CellOperation) {
      let table = self.superview?.superview as UITableView
    
      // "table" is outer feed table
      // self is the PostCell that is updating it's comments
      // self.comments is UITableView for displaying comments inside of the PostCell
      table.beginUpdates()
      self.comments.beginUpdates()
    
      // This function handles inserting/removing/reloading a range of comments
      // so we build out an array of index paths for each row that needs updating
      var indexPaths = [NSIndexPath]()
      for var index = startRow; index <= endRow; index++ {
        indexPaths.append(NSIndexPath(forRow: index, inSection: 0))
      }
    
      switch operation {
      case .INSERT:
        self.comments.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
      case .DELETE:
        self.comments.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
      case .RELOAD:
        self.comments.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
      }
    
      self.comments.endUpdates()
      table.endUpdates()
    
      // trigger a call to updateConstraints so that we can update the height constraint 
      // of the comments table to fit all of the comments
      self.setNeedsUpdateConstraints()
    }
    
    override func updateConstraints() {
      super.updateConstraints()
      self.commentsHeight.constant = self.comments.sizeThatFits(UILayoutFittingCompressedSize).height
    }
    

    This accomplishes the update just fine. The post is updated in place with comments added or removed inside of the PostCell as expected. I am using auto sizing PostCells in the feed table. The comments table of the PostCell expands to show all of the comments, but the animation is a bit jerky and the table sort of scrolls up and down a dozen pixels or so while the cell update animation takes place.

    The jumping during resizing is a bit annoying, but my main issue comes afterwards. Now if I scroll down in the feed, the scrolling is smooth as before, but if I scroll up above the cell I just resized after adding comments, the feed will jump backwards a few times before it reaches the top of the feed. I setup iOS8 auto sizing cells for the Feed like this:

    // (FeedController.swift)
    // tableView is the feed table containing PostCells
    self.tableView.rowHeight = UITableViewAutomaticDimension
    self.tableView.estimatedRowHeight = 560
    

    If I remove the estimatedRowHeight, the table just scrolls to the top anytime a cell height changes. I'm feeling pretty stuck on this now and as a new iOS developer, could use any tips you might have.

  • Bryan Alger
    Bryan Alger over 9 years
    I ended up implementing the heightForRowAtIndexPath method and caching the result to improve performance since it's a bit involved and the feed could be long. When a user adds/deletes or loads comments on one of the posts in the feed, I invalidate the height calculation for that cell so that it is recalculated during scroll. Jerkiness is gone, I wish I could simplify the code and leverage the new height calculation features, but I could not get it to work well enough with my TableViewCell
  • Gabriel Cartier
    Gabriel Cartier over 9 years
    Have you tried with the method I described above? This is the way it should be done with iOS 8, the height should not be calculated as the framework already takes care of it. If you implement the heightForRowAtIndexPath method, you are simply overriding the behaviour of the SDK.
  • Jamie Hamick
    Jamie Hamick about 9 years
    @BryanAlger is correct. The automatic row heights just aren't usable for tables with a lot of rows with much variance of height. For reliable smooth scrolling you must give correct results in the heightForRowAtIndexPath method, preferably with cached row heights. Otherwise you will get jerkiness when the tableView needs to update it's contentSize, especially in cases like when you push or show another view controller and come back. Unfortunately automatic row heights are only usable for simple tableViews with a few rows.
  • Gabriel Cartier
    Gabriel Cartier about 9 years
    In the case you implement heightForRowAtIndexPath, you are not using the power of automatic cell height dimension. Sure, implementing that will work, but then your cells are not dynamic.
  • Sakiboy
    Sakiboy almost 9 years
    For my case, I had already implemented cell configuration, height calculation, and height caching in heightForRowAtIndexPath, but still had a jerky UITableView scroll. Following @GabrielCartier answer and adding more specific logic depending on the type of cell really helped and solved the problem, thank you!
  • enricmacias
    enricmacias almost 9 years
    This is the solution for all the the similar problems/questions on this topic and it's not even accepted as the right answer.
  • Gabriel Cartier
    Gabriel Cartier over 8 years
    I wouldn't recommend using this method, calling auto-layout methods in a method such a cellForRowAtIndexPath could greatly affect the performance of the TableView.
  • o15a3d4l11s2
    o15a3d4l11s2 over 8 years
    Really simple and effective solution. Thanks!
  • AppreciateIt
    AppreciateIt almost 8 years
    Just curious, is there any reason I couldn't use the Swift Dictionary instead of NSMutableDictionary? This solution works great by the way, thank you!
  • adnako
    adnako almost 8 years
    @dosdos God bless you, dude!
  • TonyTony
    TonyTony almost 8 years
    For me, it still does not work, I have a self defined imageView, and based on the dimension of the width and height to update its constraint of height. Even with the cell heights cached, the scrolling is still jumping, especially before a new cell with a different height than the current one on screen is scrolled into screen.
  • jamesk
    jamesk almost 8 years
    If you're using Swift, note that a native Dictionary may perform better than NSDictionary and NSCache: packtpub.com/books/content/using-flyweight-pattern
  • AVEbrahimi
    AVEbrahimi over 7 years
    Awesome, removed flicker at all.
  • MLBDG
    MLBDG over 7 years
    Added update for SWIFT 3 (dont forget the self.tableView.rowHeight = UITableViewAutomaticDimension in viewDidLoad)
  • SuperDuperTango
    SuperDuperTango over 7 years
    @dosdos I could imagine setting the heights in cellForRowAtIndexPath instead of in tableView:willDisplayCell. Is there a reason for choosing willDisplayCell?
  • dosdos
    dosdos over 7 years
    Because it's the method where the cell is more likely to have the correct size
  • nickdnk
    nickdnk over 7 years
    Didn't really help for me, unfortunately. Auto-layout is a bit weird here.
  • Uma Madhavi
    Uma Madhavi over 7 years
    @Ranknoodle.. Thank You! This helped me
  • Uma Madhavi
    Uma Madhavi over 7 years
    @Ranknoodle.. This helped me to improve scrolling performance. But it is very much (like 10-15 seconds) to launch the application. Can u guide me what may be the reason.
  • Chad Pavliska
    Chad Pavliska about 7 years
    This appears to have resolved my bouncy tableview! I would make one edit in the Swift 3 code to use an 'if let' which looks more idiomatic swift: "if let height = self.heightAtIndexPath.object(forKey: indexPath) as? CGFloat {"
  • ndominati2
    ndominati2 about 7 years
    Fabulous! Wonderful! It's working really well!!!!!! Thank you so much! I think you can mark it as the solution.
  • Mukul More
    Mukul More almost 7 years
    Doesn't work. Tableview is bouncing even after using the above solution.
  • Gabriel Pires
    Gabriel Pires over 6 years
    I'd give you a hug if I could. This works excellently! After updating to ios 11, my tableview starting acting jumpy/jerky when inserting a cell. Thank you so much
  • Ethan G
    Ethan G over 6 years
    I was very skeptical at first...so far this is working! I'm surprised! Seems clever :)
  • Ethan G
    Ethan G over 6 years
    So I think the solution to this may need to go deeper than cached estimated heights (and with the solution above I noticed a lot of cells being initialized needlessly during auto scroll, but maybe that's a side effect of auto-dimension anyway). I just implemented a list with wide variety of heights using auto-dimension and it scrolls perfectly smoothly. I think the difference is that I'm being disciplined about creating my model for the cell ahead of time and not doing ANY data calculations when displaying the cell (data calculations + cell reuse = longer ms to display cell => jerky scrolling)
  • sudoExclaimationExclaimation
    sudoExclaimationExclaimation over 6 years
    I was skeptical but after testing it, this works PERFECT!!! Thank you so much! I gotta change my attitude "can't diss it until I try it".
  • Jovirus
    Jovirus about 6 years
    I have cells that varies from each depended on the assigned value. This doesnt solve the problem.
  • manmal
    manmal almost 6 years
    hey i edited your answer to use a typed dictionary. i just wanted to paste my own swift 4 code as an answer, but found that editing yours would suffice. hope you don't mind.
  • Darshan Mothreja
    Darshan Mothreja almost 6 years
    Saved my lots of time. Great solution (y)
  • sudoExclaimationExclaimation
    sudoExclaimationExclaimation over 5 years
    Just re-used this solution in my 5th app and works fabulously!
  • ARUN KUMAR
    ARUN KUMAR almost 5 years
    perfect solution.Thanks
  • RicardoDuarte
    RicardoDuarte over 4 years
    @dosdos you Sir rock. Feb 2020 and this is still the answer to use. I may say that i wasnt getting issues with scrolling, i was getting issues when adding some types of cells, and scrolling to them. utter shambles.