UITableView dynamic cell heights only correct after some scrolling
I have a UITableView
with a custom UITableViewCell
defined in a storyboard using auto layout. The cell has several multiline UILabels
.
The UITableView
appears to properly calculate cell heights, but for the first few cells that height isn't properly divided between the labels.
After scrolling a bit, everything works as expected (even the cells that were initially incorrect).
- (void)viewDidLoad {
[super viewDidLoad]
// ...
self.tableView.rowHeight = UITableViewAutomaticDimension;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
// ...
// Set label.text for variable length string.
return cell;
}
Is there anything that I might be missing, that is causing auto layout not to be able to do its job the first few times?
I've created a sample project which demonstrates this behaviour.
I don't know this is clearly documented or not, but adding [cell layoutIfNeeded]
before returning cell solves your problem.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
NSUInteger n1 = firstLabelWordCount[indexPath.row];
NSUInteger n2 = secondLabelWordCount[indexPath.row];
[cell setNumberOfWordsForFirstLabel:n1 secondLabel:n2];
[cell layoutIfNeeded]; // <- added
return cell;
}
This worked for me when other similar solutions did not:
override func didMoveToSuperview() {
super.didMoveToSuperview()
layoutIfNeeded()
}
This seems like an actual bug since I am very familiar with AutoLayout and how to use UITableViewAutomaticDimension, however I still occasionally come across this issue. I'm glad I finally found something that works as a workaround.
Adding [cell layoutIfNeeded]
in cellForRowAtIndexPath
does not work for cells that are initially scrolled out-of-view.
Nor does prefacing it with [cell setNeedsLayout]
.
You still have to scroll certain cells out and back into view for them to resize correctly.
This is pretty frustrating since most devs have Dynamic Type, AutoLayout and Self-Sizing Cells working properly — except for this annoying case. This bug impacts all of my "taller" table view controllers.
I had same experience in one of my projects.
Why it happens?
Cell designed in Storyboard with some width for some device. For example 400px. For example your label have same width. When it loads from storyboard it have width 400px.
Here is a problem:
tableView:heightForRowAtIndexPath:
called before cell layout it's subviews.
So it calculated height for label and cell with width 400px. But you run on device with screen, for example, 320px. And this automatically calculated height is incorrect. Just because cell's layoutSubviews
happens only after tableView:heightForRowAtIndexPath:
Even if you set preferredMaxLayoutWidth
for your label manually in layoutSubviews
it not helps.
My solution:
1) Subclass UITableView
and override dequeueReusableCellWithIdentifier:forIndexPath:
. Set cell width equal to table width and force cell's layout.
- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [super dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
CGRect cellFrame = cell.frame;
cellFrame.size.width = self.frame.size.width;
cell.frame = cellFrame;
[cell layoutIfNeeded];
return cell;
}
2) Subclass UITableViewCell
.
Set preferredMaxLayoutWidth
manually for your labels in layoutSubviews
. Also you need manually layout contentView
, because it doesn't layout automatically after cell frame change (I don't know why, but it is)
- (void)layoutSubviews {
[super layoutSubviews];
[self.contentView layoutIfNeeded];
self.yourLongTextLabel.preferredMaxLayoutWidth = self.yourLongTextLabel.width;
}
I have a similar problem, at the first load, the row height was not calculated but after some scrolling or go to another screen and i come back to this screen rows are calculated. At the first load my items are loaded from the internet and at the second load my items are loaded first from Core Data and reloaded from internet and i noticed that rows height are calculated at the reload from internet. So i noticed that when tableView.reloadData() is called during segue animation (same problem with push and present segue), row height was not calculated. So i hidden the tableview at the view initialization and put an activity loader to prevent an ugly effect to the user and i call tableView.reloadData after 300ms and now the problem is solved. I think it's a UIKit bug but this workaround make the trick.
I put theses lines (Swift 3.0) in my item load completion handler
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300), execute: {
self.tableView.isHidden = false
self.loader.stopAnimating()
self.tableView.reloadData()
})
This explain why for some people, put a reloadData in layoutSubviews solve the issue