UITableView with dynamic cell heights jumping when scrolling up after reloading cell - ios

I have a table view with the potential for each cell to have its own height, thus isn't suitable to use rowHeight. Instead, right now I'm using let indexSet = NSIndexSet(index: 10), and self.tableView.estimatedRowHeight = 75. This means that it calls the sizeThatFits function on the cell, to determine its' height. This all works well.
The problem is then when you reload a cell that's onscreen. Scrolling down to show cell 10, for example, then reloading cell 10, works fine. But when you begin to scroll back up, past the cells you've already seen, it reverts to the estimatedRowHeight for each cell, completely disregarding sizeThatFits, and so jumps around as you scroll. It'd be impossible for me to give an accurate or 'good enough' estimatedRowHeight so that this jumping wouldn't be noticeable, as my cells will be able to display either a line of text, or a full image - a big difference in size.
I've shown this effect here:
https://vid.me/edgW
I've made many different attempts at this, using a mixture of heightForRowAtIndexPath, estimatedHeightForRowAtIndexPath.. etc.. I've tried various pieces of advice on StackOverflow. Nothing seems to work.
I've attached a very simple sample project, where you can try it for yourself:
https://www.dropbox.com/s/8f1rvkx9k23q6c1/tableviewtest.zip?dl=0
Run the project.
Scroll until cell 10 is in view.
Wait up to 5 seconds for the cell to reload (it becomes purple).
Scroll up.
Worth noting - this does not happen if the cell is not in view when it's reloaded. If it's either above or below the current scroll point, everything works as expected.

This behavior appears to be a bug, if for no other reason than it's no longer reproducible on iOS 9. I'm sure that's not much consolation.
The issue primarily derives from having an inaccurate estimate, like #NickCatib said. The best you can do on iOS 8 is to improve the estimation. A technique many have recommended is to cache heights in willDisplayCell and use them on subsequent calls to estimatedRowHeightAtIndexPath.
You might be able to mitigate the behavior by not doing anything to get UITableView to discard its caches, like by modifying the content in a cell directly using cellForRowAtIndexPath rather than using reloading if it's onscreen. However, that won't help if you actually need to change the height of the cell.
I'm afraid to say the bug can't be easily be fixed within a table view, as you don't have control over the layout. The bug can be more easily worked around in a subclass UICollectionViewFlowLayout by changing the contentOffsetAdjustment during invalidation, although that might not be terribly easy.

I had this issue too and used a solution from SO which I am unable to find right now. If I do, I will add the link. Here's the solution:
The issue is with table view not having the right estimate for height of rows. To fix it, cache the height initially in willDisplayCell and next time use that height.
Code
In viewDidLoad:
heightAtIndexPath = [NSMutableDictionary new];
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *height = #(cell.frame.size.height);
[heightAtIndexPath setObject:height forKey:indexPath];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
if([heightAtIndexPath objectForKey:indexPath]) {
return [[heightAtIndexPath objectForKey:indexPath] floatValue];
} else {
return UITableViewAutomaticDimension;
}
}

In iOS 13.5.1:
I have a tableView which contains 4 types of cells , all off them having different and dynamic heights.
I have followed the accepted solution here, but to solve jumping effect I need to add more when reloading table view:
From accepted answer I have added below code:
Declare this variable
var allCellHeights = [IndexPath : CGFloat]()
Then add 2 methods:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
self.allCellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.allCellHeights[indexPath] ?? UITableView.automaticDimension
}
Now the extra code I have to add when reloading tableview:
let contentOffset = self.tableView.contentOffset
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
self.tableView.setContentOffset(contentOffset, animated: false)
also check this answer : reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling

Uh... this is kind of hard issue to deal with.
Let's take a look at facebook. They had this same issue with their timeline, and they ended up doing it in some kind of web view.
I had a similar issue with some kind of timeline, used automatic row height and had that issue. First thing to resolve it was to set estimatedHeight as close as possible to average cell height. That is pretty hard to deal with since you may have text (height 50) or images + text ( height 1500). Next thing to do with improving this was implementing estimatedHeight forIndexPath which basicly return different estimated height for different indexPaths.
After that there were a lot of other solutions but this was as closest as it can with variable heights (huge differences).

I was facing the same problem, my table works fine until the tableview reloads. So i found a solution, use only rowHeight not estimated height. Also if you have different height. So please provide the complete code so i will provide a solution. I have a cell which like instagram page. I am passing calculated height in heightforrow method which work fine. But estimated height not works fine in that situation. If you use below code, it works fine. Please try.
self.tableView.rowHeight = 75 //it will be your dynamic height
//self.tableView.estimatedRowHeight = 75
If you confuse to calculate the height for row for each cell, just post the sample i will provide the solution. If i can
Thanks

Related

How do I tell my UITableViewCell to "auto-resize" when its content changes?

In my cell.xib, I have a label, with constraints to all its sides. I've set that label to lines = 0 and line-break = word wrap. Then, I do this to my TableView:
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 100.0
Everything works great, and my UITableViewCell is auto-height. If the text is long, then my tableView intelligently calculates the size.
The problem is -- how do I tell my UITableView to "re-calculate" the size once the content changes in the cell?
My cell could call its delegate, and in this delegate, I'd like the TableView to re-draw the height.
Right now, the content in my cells change constantly, but the cell height never changes.
There is a documented way to do this. See UITableView.beginUpdates() documentation:
You can also use this method followed by the endUpdates method to animate the change in the row heights without reloading the cell.
So, the correct solution is:
tableView.beginUpdates()
tableView.endUpdates()
Also note that there is a feature that is not documented - you can add a completion handler for the update animation here, too:
tableView.beginUpdates()
CATransaction.setCompletionBlock {
// this will be called when the update animation ends
}
tableView.endUpdates()
However, tread lightly, it's not documented (but it works because UITableView uses a CATransaction for the animation).
I've found the best way to get it to check heights is to call, after whatever text change has been made, in order:
self.tableView.beginUpdates()
self.tableView.endUpdates()
This causes the tableView to check heights for all visible cells and it will cause changes to be made as needed.
I think the simplest solution is to reload that specific cell. For example:
- (void)yourDelegateMethodOfCell:(UITableViewCell *)cell {
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
//If cell is not visible then indexPath will be nil so,
if (indexPath) {
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
You can get automatic cell height by this code
tableView.beginUpdates()
// add label text update code here
// label.numberOfLines = label.numberOfLines == 0 ? 1 : 0
tableView.endUpdates()
Below is the reference to this solution with demo :
GitHub-RayFix-MultiLineDemo
I got your point, and I met the problem before.
Your cell is in AutoLayout, and you wish the cell changes by itself. Here is recommended answer for that: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights , so we don't talk about that again.
So here we focus on your problem.
Since the content of your cell changes constantly, which means the content has updated. Here we suppose the content is a label. We set the label's text, and surely the label's height maybe change.
Here comes the point: How does the label's change inform the cell to update?
We use AutoLayout, surely we have to update the constraint of height for the label.
And I think it will work!
Below is the detail step:
1. We setup the constraints for the cell's subviews.(I think it's done)
2. One of the label's height is changed by itself.(I think it's done too)
3. We get the new height of the label, and update the constraint of height for the label.(what we have to do)
Seems you wanted to reload the particular cell/cells based on content changes
Here we have a couple of options
1) Need to reload the entire table view .
or else
2) Reload particular cell/cells based on content changes.
But the preferred option would be reloading the particular cell,
Why because
when you asked your UITableView instance to reload a couple of cells,tableview will asks its datasource(-tableView:cellForRowAtIndexPath:) to get the updated content,so that the reloaded cells will have the updated size & Updated content aswell.
Try to reload the cells when the content/height need to update based on content
Hope that helps!
Happy coding :)
Take a look at this answer : Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
How to achieve dynamic cell size is described very thorough there.
As a suggestion for testing try adding setNeedsLayout to:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
or
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
To disable the annoying tableView animation:
UIView.setAnimationsEnabled(false)
tableView.beginUpdates()
// cell.titleLabel?.text = "title"
// cell.detailTextLabel?.text = "Very long text ..."
// cell.detailTextLabel?.numberOfLines = 0
tableView.endUpdates()
UIView.setAnimationsEnabled(true)
You can resize your cell height by implementing below method only
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewAutomaticDimension;
}

How to set height of row depend on 80 percentage height of main View/Display

I have a view controller and a tableview inside of that. I want to set height of the row 80% of main view, Is there any method without using autolayout? Please help me to find the proper solution.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return self.view.frame.height * 0.8
}
substituted self.view for your mainView
ObjC if you need it:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return (self.view.frame.size.height * 0.8);
}
The only problem with this answer, as Michael above has sort of indicated, there's absolutely nothing wrong with constraints and you really should be doing something like this with constraints. There's no guarantee that my solution is going to work for you because I can't garantee when the system is going to trigger a call to the cell height vs the mainView, and if the mainView's height isn't triggered FIRST, the you have a big problem on your hand because now your cells are height ZERO. It would be best, if you need a custom height for a cell, to subclass a cell, use autolayout like a boss, and do this the RIGHT way, but feel free to implement this method, it will work, but you now know when it won't work. Good luck
And should it not work, just say something, there's always people more than willing to help you through this, it's no big deal!

iOS 8 UITableView first row has wrong height

I'm working on an app where I face a strange issue. I've created a UITableViewController in the storyboard and added a prototype cell. In this cell, I've added an UILabel element and this UILabel takes up the whole cell. I've set it up with Auto Layout and added left, right, top and bottom constraints. The UILabel contains some text.
Now in my code, I initialize the the rowHeight and estimatedRowHeight of the table view:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 50
}
And I create the cell as follows:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("HelpCell") as? UITableViewCell
if(cell == nil) {
cell = UITableViewCell(style: .Default, reuseIdentifier: "HelpCell")
}
return cell!
}
I return two rows in my table view. Here comes my problem: the height of the first row is way to large. It appear that the second, third row etc all have a correct height. I really don't understand why this is the case. Can someone help me with this?
I had a problem where the cells' height were not correct on the first load, but after scrolling up-and-down the cells' height were fixed.
I tried all of the different 'fixes' for this problem and then eventually found that calling these functions after initially calling self.tableView.reloadData.
self.tableView.reloadData()
// Bug in 8.0+ where need to call the following three methods in order to get the tableView to correctly size the tableViewCells on the initial load.
self.tableView.setNeedsLayout()
self.tableView.layoutIfNeeded()
self.tableView.reloadData()
Only do these extra layout calls after the initial load.
I found this very helpful information here: https://github.com/smileyborg/TableViewCellWithAutoLayoutiOS8/issues/10
Update:
Sometimes you might have to also completely configure your cell in heightForRowAtIndexPath and then return the calculated cell height. Check out this link for a good example of that, http://www.raywenderlich.com/73602/dynamic-table-view-cell-height-auto-layout , specifically the part on heightForRowAtIndexPath.
Update 2: I've also found it VERY beneficial to override estimatedHeightForRowAtIndexPath and supply somewhat accurate row height estimates. This is very helpful if you have a UITableView with cells that can be all kinds of different heights.
Here's a contrived sample implementation of estimatedHeightForRowAtIndexPath:
public override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! MyCell
switch cell.type {
case .Small:
return kSmallHeight
case .Medium:
return kMediumHeight
case .Large:
return kLargeHeight
default:
break
}
return UITableViewAutomaticDimension
}
Update 3: UITableViewAutomaticDimension has been fixed for iOS 9 (woo-hoo!). So you're cells should automatically size themselves without having to calculate the cells height manually.
As the Apple says in the description of setNeedsLayout:
This method does not force an immediate update, but instead waits for the next update cycle, you can use it to invalidate the layout of multiple views before any of those views are updated. This behavior allows you to consolidate all of your layout updates to one update cycle, which is usually better for performance.
Because of this you should add needed lines of code (which should be executed with right layout) in dispatch_after block(which will put your method in queue of RunLoop). And your code will be executed after needs layout applies.
Example:
- (void)someMethod {
[self.tableView reloadData];
[self.tableView setNeedsLayout];
[self.tableView layoutIfNeeded];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//code which should be executed with the right size of table
});
In iOS 8, assigning the estimatedRowHeight a value turns on the new iOS 8 automatic row height calculation feature. This means that the cell's height is derived by using its internal constraints from the inside out. If there's something wrong with those constraints you'll get odd results. So there's something wrong with your cell constraints. They are probably ambiguous; that is the usual reason for inconsistency. However that's all I can tell you, since you have not actually shown / described the constraints.
I suggest removing the bottom constraint on the UILabel. It will resize according to the text and the cell should resize as well.
If that didn't resolve the issue, try adding the following in viewDidLoad() :
self.tableView.reloadData()
This worked for me
- (void)layoutSubviews
{
[super layoutSubviews];
// Your implementation
}
I got this from here >> http://askstop.com/questions/2572338/uitableview-displays-separator-at-wrong-position-in-ios8-for-some-cells

IOS 8 UITableView self-sizing cells jump/shift visually when a new view controller is pushed

Here's how I set my table:
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.estimatedRowHeight = 150;
[self.view addSubview:self.tableView];
Within my cells, I call - (CGSize)sizeThatFits:(CGSize)size to programmatically return the height (which is set in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath. I'm not using constraints or autolayout.
Nothing complex and my heights are all perfectly laid out visually.
However the problem is when I push a new view controller, the cells jump/shift visually (either up or down). It seems to be jumping based on calculating the estimated row height values - yet sizeThatFits is also called for each visible cell before shifting so I'm really confused (not sure why either needs to be called at all really, since I'm leaving the view). I've checked the contentOffset for the tableView - it's unchanged so it's not the problem.
Okay, I solved it by caching my cell heights in sizeThatFits, and returning that value for estimated cell heights within the delegate. Works beautifully.
Quick fix:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.tableView.reloadData()
}
Edit: After spending hours on this and similar issues I've found the best solution is to cache the cell heights and return them in estimatedHeightForRowAtIndexPath delegate method, the problem is caused by estimated heights being really inaccurate.
I cached the heights in tableView(_:willDisplayCell:forRowAtIndexPath:) into a dictionary with the unique ID's for the data as keys this way when data gets added or updated I can just remove that height from the cache and use better estimated heights so only that cell uses an estimated height. So far this solves all my jumping and scrolling issues.

UITableView layout messing up on push segue and return. (iOS 8, Xcode beta 5, Swift)

tldr; Auto constrains appear to break on push segue and return to view for custom cells
Edit: I have provided a github example project that shows off the error that occurs
https://github.com/Matthew-Kempson/TableViewExample.git
I am creating an app which requires the title label of the custom UITableCell to allow for varying lines dependent on the length of the post title. The cells load into the view correctly but if I press on a cell to load the post in a push segue to a view containing a WKWebView you can see, as shown in the screen shot, the cells move immediately to incorrect positions. This is also viewed when loading the view back through the back button of the UINavigationController.
In this particular example I pressed on the very end cell, with the title "Two buddies I took a picture of in Paris", and everything is loaded correctly. Then as shown in the next screenshot the cells all move upwards for unknown reasons in the background of loading the second view. Then when I load the view back you can see the screen has shifted upwards slightly and I cannot actually scroll any lower than is shown. This appears to be random as with other tests when the view loads back there is white space under the bottom cell that does not disappear.
I have also included a picture containing the constraints that the cells has.
Images (I need more reputation to provide images in this question apparently so they are in this imgur album): http://imgur.com/a/gY87E
My code:
Method in custom cell to allow the cell to resize the view correctly when rotating:
override func layoutSubviews() {
super.layoutSubviews()
self.contentView.layoutIfNeeded()
// Update the label constaints
self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.frame.width
self.detailsLabel.preferredMaxLayoutWidth = self.detailsLabel.frame.width
}
Code in tableview
override func viewDidLoad() {
super.viewDidLoad()
// Create and register the custom cell
self.tableView.estimatedRowHeight = 56
self.tableView.rowHeight = UITableViewAutomaticDimension
}
Code to create the cell
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
if let cell = tableView.dequeueReusableCellWithIdentifier("LinkCell", forIndexPath: indexPath) as? LinkTableViewCell {
// Retrieve the post and set details
let link: Link = self.linksArray.objectAtIndex(indexPath.row) as Link
cell.titleLabel.text = link.title
cell.scoreLabel.text = "\(link.score)"
cell.detailsLabel.text = link.stringCreatedTimeIntervalSinceNow() + " ago by " + link.author + " to /r/" + link.subreddit
return cell
}
return nil
}
If you require any more code or information please ask and I shall provide what is necessary
Thanks for your help!
This bug is caused by having no tableView:estimatedHeightForRowAtIndexPath: method. It's an optional part of the UITableViewDelegate protocol.
This isn't how it's supposed to work. Apple's documentation says:
Providing an estimate the height of rows can improve the user experience when loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights and so lead to a longer load time. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
So this method is supposed to be optional. You'd think if you skipped it, it would fall back on the accurate tableView:heightForRowAtIndexPath:, right? But if you skip it on iOS 8, you'll get this behaviour.
What seems to be happening? I have no internal knowledge, but it looks like if you do not implement this method, the UITableView will treat that as an estimated row height of 0. It will compensate for this somewhat (and, at least in some cases, complain in the log), but you'll still see an incorrect size. This is quite obviously a bug in UITableView. You see this bug in some of Apple's apps, including something as basic as Settings.
So how do you fix it? Provide the method! Implement tableView: estimatedHeightForRowAtIndexPath:. If you don't have a better (and fast) estimate, just return UITableViewAutomaticDimension. That will fix this bug completely.
Like this:
- (CGFloat)tableView:(UITableView *)tableView
estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
There are potential side effects. You're providing a very rough estimate. If you see consequences from this (possibly cells shifting size as you scroll), you can try to return a more accurate estimate. (Remember, though: estimate.)
That said, this method is not supposed to return a perfect size, just a good enough size. Speed is more important than accuracy. And while I spotted a few scrolling glitches in the Simulator there were none in any of my apps on the actual device, either the iPhone or iPad. (I actually tried writing a more accurate estimate. But it's hard to balance speed and accuracy, and there was simply no observable difference in any of my apps. They all worked exactly as well as just returning UITableViewAutomaticDimension, which was simpler and was enough to fix the bug.)
So I suggest you do not try to do more unless more is required. Doing more if it is not required is more likely to cause bugs than fix them. You could end up returning 0 in some cases, and depending on when you return it that could lead to the original problem reappearing.
The reason Kai's answer above appears to work is that it implements tableView:estimatedHeightForRowAtIndexPath: and thus avoids the assumption of 0. And it does not return 0 when the view is disappearing. That said, Kai's answer is overly complicated, slow, and no more accurate than just returning UITableViewAutomaticDimension. (But, again, thanks Kai. I'd never have figured this out if I hadn't seen your answer and been inspired to pull it apart and figure out why it works.)]
Note that you may also need to force layout of the cell. You'd think iOS would do this automatically when you return the cell, but it doesn't always. (I will edit this once I investigate a bit more to figure out when you need to do this.)
If you need to do this, use this code before return cell;:
[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
The problem of this behavior is when you push a segue the tableView will call the estimatedHeightForRowAtIndexPath for the visible cells and reset the cell height to a default value. This happens after the viewWillDisappear call. If you come back to TableView all the visible cells are messed up..
I solved this problem with a estimatedCellHeightCache. I simply add this code snipped to the cellForRowAtIndexPath method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
// put estimated cell height in cache if needed
if (![self isEstimatedRowHeightInCache:indexPath]) {
CGSize cellSize = [cell systemLayoutSizeFittingSize:CGSizeMake(self.view.frame.size.width, 0) withHorizontalFittingPriority:1000.0 verticalFittingPriority:50.0];
[self putEstimatedCellHeightToCache:indexPath height:cellSize.height];
}
...
}
Now you have to implement the estimatedHeightForRowAtIndexPath as following:
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self getEstimatedCellHeightFromCache:indexPath defaultHeight:41.5];
}
Configure the Cache
Add this property to your .h file:
#property NSMutableDictionary *estimatedRowHeightCache;
Implement methods to put/get/reset.. the cache:
#pragma mark - estimated height cache methods
// put height to cache
- (void) putEstimatedCellHeightToCache:(NSIndexPath *) indexPath height:(CGFloat) height {
[self initEstimatedRowHeightCacheIfNeeded];
[self.estimatedRowHeightCache setValue:[[NSNumber alloc] initWithFloat:height] forKey:[NSString stringWithFormat:#"%d", indexPath.row]];
}
// get height from cache
- (CGFloat) getEstimatedCellHeightFromCache:(NSIndexPath *) indexPath defaultHeight:(CGFloat) defaultHeight {
[self initEstimatedRowHeightCacheIfNeeded];
NSNumber *estimatedHeight = [self.estimatedRowHeightCache valueForKey:[NSString stringWithFormat:#"%d", indexPath.row]];
if (estimatedHeight != nil) {
//NSLog(#"cached: %f", [estimatedHeight floatValue]);
return [estimatedHeight floatValue];
}
//NSLog(#"not cached: %f", defaultHeight);
return defaultHeight;
}
// check if height is on cache
- (BOOL) isEstimatedRowHeightInCache:(NSIndexPath *) indexPath {
if ([self getEstimatedCellHeightFromCache:indexPath defaultHeight:0] > 0) {
return YES;
}
return NO;
}
// init cache
-(void) initEstimatedRowHeightCacheIfNeeded {
if (self.estimatedRowHeightCache == nil) {
self.estimatedRowHeightCache = [[NSMutableDictionary alloc] init];
}
}
// custom [self.tableView reloadData]
-(void) tableViewReloadData {
// clear cache on reload
self.estimatedRowHeightCache = [[NSMutableDictionary alloc] init];
[self.tableView reloadData];
}
I had the exact same problem. The table view had several different cell classes, each of which was a different height. Moreover, one of the cells classes had to show additional text, meaning further variation.
Scrolling was perfect in most situations. However, the same problem described in the question manifested. That was, having selected a table cell and presented another view controller, on return to the original table view, the upwards scrolling was extremely jerky.
The first line of investigation was to consider why data was being reloaded at all. Having experimented, I can confirm that on return to the table view, data is reloaded, albeit not using reloadData.
See my comment ios 8 tableview reloads automatically when view appears after pop
With no mechanism to deactivate this behaviour, the next line of approach was to investigate the jerky scrolling.
I came to the conclusion that the estimates returned by estimatedHeightForRowAtIndexPath are an estimated precalculation. Log to console out the estimates and you'll see that the delegate method is queried for every row when the table view first appears. That's before any scrolling.
I quickly discovered that some of the height estimate logic in my code was badly wrong. Resolving this fixed the worst of the jarring.
To achieve perfect scrolling, I took a slightly different approach to the answers above. The heights were cached, but the values used were from the actual heights that would have been captured as the user scrolls downwards:
var myRowHeightEstimateCache = [String:CGFloat]()
To store:
func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
myRowHeightEstimateCache["\(indexPath.row)"] = CGRectGetHeight(cell.frame)
}
Using from the cache:
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
if let height = myRowHeightEstimateCache["\(indexPath.row)"]
{
return height
}
else
{
// Not in cache
... try to figure out estimate
}
Note that in the method above, you will need to return some estimate, as that method will of course be called before didEndDisplayingCell.
My guess is that there is some sort of Apple bug underneath all of this. That's why this issue only manifests in an exit scenario.
Bottom line is that this solution is very similar to those above. However, I avoid any tricky calculations and make use of the UITableViewAutomaticDimension behaviour to just cache the actual row heights displayed using didEndDisplayingCell.
TLDR: work around what's most likely a UIKit defect by caching the actual row heights. Then query your cache as the first option in the estimation method.
Well, until it works, you can delete those two line:
self.tableView.estimatedRowHeight = 45
self.tableView.rowHeight = UITableViewAutomaticDimension
And add this method to your viewController:
override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as TableViewCell
cell.cellLabel.text = self.tableArray[indexPath.row]
//Leading space to container margin constraint: 0, Trailling space to container margin constraint: 0
let width = tableView.frame.size.width - 0
let size = cell.cellLabel.sizeThatFits(CGSizeMake(width, CGFloat(FLT_MAX)))
//Top space to container margin constraint: 0, Bottom space to container margin constraint: 0, cell line: 1
let height = size.height + 1
return (height <= 45) ? 45 : height
}
It worked without any other changes in your test project.
If you have set tableView's estimatedRowHeight property.
tableView.estimatedRowHeight = 100;
Then comment it.
// tableView.estimatedRowHeight = 100;
It solved the bug which occurs in iOS8.1 for me.
If you really want to keep it,then you could force tableView to reloadData before pushing.
[self.tableView reloadData];
[self.navigationController pushViewController:vc animated:YES];
or do it in viewWillDisappear:.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.tableView reloadData];
}
Hope it helps.
In xcode 6 final for me the workaround does not work. I am using custom cells and dequeing a cell in heightForCell leads to infinity loop. As dequeing a cell calls heightForCell.
And still the bug seems to be present.
If none of the above worked for you (as it happened to me) just check the estimatedRowHeight property from the table view is kind of accurate. I checked I was using 50 pixels when it was actually closer to 150 pixels. Updating this value fixed the issue!
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = tableViewEstimatedRowHeight // This should be accurate.

Resources