Jerky scrolling after updating UITableViewCell with UITableViewAutomaticDimension

I am creating an application that has a feed view for user posts. This view has a UITableView with a custom implementation of UITableViewCell . Inside this cell, I have another UITableView to display comments. The bottom line is:

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

The original feed will be loaded with 3 comments for preview, but if there are more comments or if the user adds or removes a comment, I want to update PostCell in place inside the feed table view by adding or removing CommentCells to the comment table inside PostCell . I am currently using the following helper to accomplish this:

 // (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 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 is great for updating. The post is updated in-place with comments added or deleted inside PostCell as expected. I use PostCells auto- PostCells in the feed table. The PostCell comment PostCell expands to display all the comments, but the animation is a bit jerky, and the table sorts the scroll up and down by a dozen pixels or so while the cell update animation is happening.

Leaping when resizing is a bit annoying, but my main problem comes later. Now, if I scroll down in the feed, the scroll will be as smooth as before, but if I scroll up above the cell, I just resized after adding comments, the feed jumps back several times before it reaches the top of the feed. I configure iOS8 automatic iOS8 cells for Feed as follows:

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

If I delete the estimatedRowHeight , the table simply scrolls to the beginning anytime the cell height changes. I feel pretty stuck on this now and as a new iOS developer, I could use whatever advice you might have.

+61
ios uitableview ios8 swift
Jan 17 '15 at 5:22
source share
6 answers

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

It consists of saving each height of the dictionary and updating (in the dictionary) as the tableView displays the cell.

Then you will return the stored height in the method - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath .

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) } 
+94
Oct 28 '15 at 17:11
source share

We had the same problem. This is due to a poor estimate of the cell height, which causes the SDK to force a poor height, which will cause the cells to jump when scrolling backward. Depending on how you built your cell, the best way to fix this is to implement the UITableViewDelegate - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath

As long as your estimate is pretty close to the real value of the cell height, it almost cancels jumping and jerking. Here, as we implemented it, you 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 support for Swift 2

 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 } 
+28
Feb 06 '15 at 22:54
source share

dosdos answer worked for me in Swift 2

Declare 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 = NSMutableDictionary() func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { let height = self.heightAtIndexPath.object(forKey: indexPath) if ((height) != nil) { return CGFloat(height as! CGFloat) } else { return UITableViewAutomaticDimension } } func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { let height = cell.frame.size.height self.heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying) } 
+15
May 9 '16 at 17:04
source share

I also had a problem. I found a workaround, but it did not completely fix the jerk. But that seems a lot better than the previous choppy scrolling.

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

 cell.setNeedsUpdateConstraints() cell.updateConstraintsIfNeeded() 

EDIT: You may need to play with the value of tableView.estimatedRowHeight to get a smoother scroll.

+2
Jul 29 '15 at 6:38
source share

Following @ dosdos answer.

I also found it interesting to implement: tableView(tableView: didEndDisplayingCell: forRowAtIndexPath:

Especially for my code, where the cell dynamically changes the constraints while the cell is already displayed on the screen. Updating such a dictionary helps for the second time display the cell.

 var heightAtIndexPath = [NSIndexPath : NSNumber]() .... tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = UITableViewAutomaticDimension .... extension TableViewViewController: UITableViewDelegate { //MARK: - UITableViewDelegate func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { let height = heightAtIndexPath[indexPath] if let height = height { return CGFloat(height) } else { return UITableViewAutomaticDimension } } func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { let height: NSNumber = CGRectGetHeight(cell.frame) heightAtIndexPath[indexPath] = height } func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { let height: NSNumber = CGRectGetHeight(cell.frame) heightAtIndexPath[indexPath] = height } } 
0
Aug 26 '16 at 15:30
source share
Decision

@dosdos is working fine

but there is something you must add

after @ 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 these lines whenever you want, for me I use it inside textDidChange

  • first reboot Tableview
  • update restriction
  • finally go to the beginning of Tableview

     tableView.reloadData() self.tableView.layoutIfNeeded() self.tableView.setContentOffset(CGPoint.zero, animated: true) 
0
Sep 16 '17 at 9:56 on
source share



All Articles