UITableView dynamic cell heights only correct after some scrolling

69,929

Solution 1

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;
}

Solution 2

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.

Solution 3

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.

Solution 4

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;
}

Solution 5

none of the above solutions worked for me, what worked is this recipe of a magic: call them in this order:

tableView.reloadData()
tableView.layoutIfNeeded() tableView.beginUpdates() tableView.endUpdates()

my tableView data are populated from a web service, in the call back of the connection I write the above lines.

Share:
69,929
blackp
Author by

blackp

Updated on September 07, 2021

Comments

  • blackp
    blackp over 2 years

    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.

    Sample project: Top of table view from sample project on first load. Sample project: Same cells after scrolling down and back up.

  • blackp
    blackp over 9 years
    I do wonder though, why is it only necessary the first time through? It works just as well if I put the call to -layoutIfNeeded in the the cell's -awakeFromNib. I'd prefer to only call layoutIfNeeded where I know why it's necessary.
  • BonanzaDriver
    BonanzaDriver over 9 years
    Actually Ramesh, this shouldn't be needed as long as the tableView.estimatedRowHeight and tableView.rowHeight = UITableViewAutomaticDimension properties are set. Also, make sure the custom cell has appropriate constraints on the various widgets and the contentView.
  • Mustafa
    Mustafa over 9 years
    Worked for me! And I would love to know why is it required!
  • Andrei Konstantinov
    Andrei Konstantinov almost 9 years
    Did not work if you render size class, that different from any X any
  • Max
    Max almost 9 years
    Same for me. when I first scroll, cell's size is 44px. if I scroll to make the cell out of sight and come back, it is sized properly. Did you find any solution?
  • ImpurestClub
    ImpurestClub over 8 years
    Thank you @ashy_32bit this solved it for me as well.
  • mckeejm
    mckeejm over 8 years
    this worked for me, without applying any of the other solutions listed above. Great find.
  • Fattie
    Fattie over 8 years
    it would seem to be a plain bug, @Scenario right? horrible stuff.
  • ypresto
    ypresto over 8 years
    Using [cell layoutSubviews] instead of layoutIfNeeded might be possible fix. Refer stackoverflow.com/a/33515872/1474113
  • z22
    z22 over 8 years
    @AndreyKonstantinov Yup it doesn't work with size class, how do I make it work with size class?
  • GK100
    GK100 almost 8 years
    When I ran into this issue, I also needed to ensure the tableview's frame was correct, so I called [self.tableview layoutIfNeeded] before the tableview was populated (in viewDidLoad).
  • Yuvrajsinh
    Yuvrajsinh over 7 years
    It works without size class, any solution with size class?
  • 6axter82
    6axter82 about 7 years
    This helped me (with size class). Just after you request and set the label's text: cell.labelText.preferredMaxLayoutWidth = cell.labelText.frame.width
  • Robert Wagstaff
    Robert Wagstaff about 7 years
    Better than the accepted answer as it is only called when the cell is being loaded from the xib instead of every cell display. Far less lines of code too.
  • Jimmy George Thomas
    Jimmy George Thomas almost 7 years
    working and definitely better than invoking reloadData twice
  • cicerocamargo
    cicerocamargo over 6 years
    Don't forget to call super.didMoveToSuperview()
  • lucius degeer
    lucius degeer over 6 years
    But this autoshrinks the text... why would one want to have different sizes of text in a table view? Looks terrible.
  • Martin
    Martin over 6 years
    which is NOT the recommended way to initialize a cell. willDisplay will have better performance than cellForRowAt. Use the lastest only to instanciate the right cell.
  • Martin
    Martin over 6 years
    Black magic. Doing it in viewWillAppear did not work for me, but doing it in viewDidAppear did.
  • Bptstmlgt
    Bptstmlgt over 6 years
    I can't believe I've spent so much time to finally find this working answer. :)
  • PJ_Finnegan
    PJ_Finnegan over 6 years
    This is the only WA that worked for me too. Except I don't use isHidden, but set the table's alpha to 0 when adding the data source, then to 1 after reloading.
  • PJ_Finnegan
    PJ_Finnegan over 6 years
    It's important to set estimatedRowHeight to a value > 0 and not to UITableViewAutomaticDimension (which is -1), otherwise the auto row height won't work.
  • richardpiazza
    richardpiazza over 6 years
    This solution 'did' work for me when implemented in the viewDidAppear. It is by no means optimal for the user experience as the table content jumps around as the correct sizing occurs.
  • axunic
    axunic about 6 years
    i want to give you this world... :)
  • andrew k
    andrew k almost 6 years
    My similar reloadData/beginUpdates/endUpdates/reloadData is also working; reloadData must be called a second time. Don't need to wrap it in async.
  • Ivan Smetanin
    Ivan Smetanin over 5 years
    @cicerocamargo Apple says about didMoveToSuperview: "The default implementation of this method does nothing."
  • TheTravloper
    TheTravloper over 5 years
    this was far far far far far most better solution that helped me. It didn't corrected the 100% of cell but upto 80% were taking their actual size. Thanks a lot
  • TNguyen
    TNguyen about 5 years
    @IvanSmetanin it doesn't matter if the default implementation doesn't do anything, you should still call the super method as it could actually do something in the future.
  • Fattie
    Fattie over 4 years
    (Just btw you should DEFINITELY call super. on that one. I have never seen a project where the team overall doesn't subclass more than once cells, so, of course you have to be sure to pick them all up. AND as a separate issue just what TNguyen said.)
  • Julian Wagner
    Julian Wagner over 4 years
    Is that supposed to be in TableView or in the Cell?
  • Shakeel Ahmed
    Shakeel Ahmed over 4 years
    amazing i have tried alot of examples but fails you are demon
  • tamtoum1987
    tamtoum1987 over 4 years
    i tried all others solution, ony your solution worked for thank you for sharing it
  • A.J. Hernandez
    A.J. Hernandez about 4 years
    Same as @martin
  • Martin
    Martin about 4 years
    2 years after, I'm reading that old comment I wrote down. This was a bad advice. Even if willDisplay have better performance, it iS recommended to initialise your cell UI in cellForRowAt when the layout is automatic. Indeed, the layout is computed by UIKit after cellForRowAt et before willDisplay. So if your cell height depends of its content, initialize the label content (or whatever) in cellForRowAt.
  • Govani Dhruv Vijaykumar
    Govani Dhruv Vijaykumar almost 4 years
    For swift 5 this worked even in collection view, god bless you, bro.
  • Darrow Hartman
    Darrow Hartman almost 4 years
    For me, this returned the correct height and layout of the cell, but it had no data in it
  • JAHelia
    JAHelia almost 4 years
    Try setNeedsDisplay() after those lines
  • KoreanXcodeWorker
    KoreanXcodeWorker almost 4 years
    This ONLY helped me working great. The above all did nothing to me(using tableview cell in another tableview cell, Swift 4.0).
  • Cam Connor
    Cam Connor almost 4 years
    I never stopped to think it had to do with an incorrect width. cell.frame.size.width = tableview.frame.width then cell.layoutIfNeeded() in the cellForRowAt function did the trick for me
  • Nazar Medeiros
    Nazar Medeiros almost 4 years
    I also have an async block but it is needed? Is there no alternative?
  • KSR
    KSR over 3 years
    This helped me a lot. Thank you rintaro.
  • Zaheer Moola
    Zaheer Moola over 2 years
    I am so glad I scrolled far down enough. This was the only solution for me. All I needed was the first part. Not a fan of reloading tableView twice but it certainly works
  • Bucket
    Bucket over 2 years
    @JAHelia Thank you brother, it works !!!