Jerky Scrolling After Updating UITableViewCell in place with UITableViewAutomaticDimension
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
- first reload Tableview
- update constraint
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.
Related videos on Youtube
Bryan Alger
Updated on May 30, 2020Comments
-
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 customUITableViewCell
implementation. Inside this cell, I have anotherUITableView
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 removingCommentCells
to the comments table inside of thePostCell
. 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 sizingPostCells
in the feed table. The comments table of thePostCell
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 over 9 yearsI 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 over 9 yearsHave 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 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 about 9 yearsIn 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 almost 9 yearsFor my case, I had already implemented cell configuration, height calculation, and height caching in
heightForRowAtIndexPath
, but still had a jerkyUITableView
scroll. Following @GabrielCartier answer and adding more specific logic depending on the type of cell really helped and solved the problem, thank you! -
enricmacias almost 9 yearsThis 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 over 8 yearsI 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 over 8 yearsReally simple and effective solution. Thanks!
-
AppreciateIt almost 8 yearsJust 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 almost 8 years@dosdos God bless you, dude!
-
TonyTony almost 8 yearsFor 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 almost 8 yearsIf you're using Swift, note that a native Dictionary may perform better than NSDictionary and NSCache: packtpub.com/books/content/using-flyweight-pattern
-
AVEbrahimi over 7 yearsAwesome, removed flicker at all.
-
MLBDG over 7 yearsAdded update for SWIFT 3 (dont forget the self.tableView.rowHeight = UITableViewAutomaticDimension in viewDidLoad)
-
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 over 7 yearsBecause it's the method where the cell is more likely to have the correct size
-
nickdnk over 7 yearsDidn't really help for me, unfortunately. Auto-layout is a bit weird here.
-
Uma Madhavi over 7 years@Ranknoodle.. Thank You! This helped me
-
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 about 7 yearsThis 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 about 7 yearsFabulous! Wonderful! It's working really well!!!!!! Thank you so much! I think you can mark it as the solution.
-
Mukul More almost 7 yearsDoesn't work. Tableview is bouncing even after using the above solution.
-
Gabriel Pires over 6 yearsI'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 over 6 yearsI was very skeptical at first...so far this is working! I'm surprised! Seems clever :)
-
Ethan G over 6 yearsSo 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 over 6 yearsI 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 about 6 yearsI have cells that varies from each depended on the assigned value. This doesnt solve the problem.
-
manmal almost 6 yearshey 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 almost 6 yearsSaved my lots of time. Great solution (y)
-
sudoExclaimationExclaimation over 5 yearsJust re-used this solution in my 5th app and works fabulously!
-
ARUN KUMAR almost 5 yearsperfect solution.Thanks
-
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.