132

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.

Blazej SLEBODA
  • 8,936
  • 7
  • 53
  • 93
blackp
  • 1,752
  • 3
  • 13
  • 14
  • 1
    I am also facing this issue,But the below answer is not working for me,Any help on this – Shangari C Jun 28 '16 at 12:07
  • 1
    facing the same issue adding layoutIfNeeded for cell doesn't work me as well ? any more suggestions – Max Sep 05 '16 at 13:01
  • 2
    Great spot. This is still a problem in 2019 :/ – Fattie Oct 10 '19 at 19:37
  • I found what helped me in the following link - it's a pretty comprehensive discussion of variable height table view cells: https://stackoverflow.com/a/18746930/826946 – Andy Weinstein Apr 22 '20 at 15:05

30 Answers30

155

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;
}
rintaro
  • 51,423
  • 14
  • 131
  • 139
  • 3
    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. – blackp Sep 23 '14 at 00:33
  • 3
    Worked for me! And I would love to know why is it required! – Mustafa Jan 16 '15 at 11:22
  • Did not work if you render size class, that different from any X any – Andrei Konstantinov May 07 '15 at 21:49
  • @AndreyKonstantinov Yup it doesn't work with size class, how do I make it work with size class? – z22 Nov 16 '15 at 07:24
  • It works without size class, any solution with size class? – Yuvrajsinh Aug 02 '16 at 10:24
  • This helped me (with size class). Just after you request and set the label's text: `cell.labelText.preferredMaxLayoutWidth = cell.labelText.frame.width` – 6axter82 Jan 30 '17 at 11:39
  • I can't believe I've spent so much time to finally find this working answer. :) – Bptstmlgt Nov 06 '17 at 17:25
  • i want to give you this world... :) – axunic Mar 28 '18 at 16:27
  • This helped me a lot. Thank you rintaro. – KSR Dec 02 '20 at 07:06
  • If your content gets updated asynchronously later, the height will already have been set. As well as `layoutIfNeeded()` you may need to call `invalidateIntrinsicContentSize()`. I put both in a `defer {}` block at the start of my update method. – alex bird Apr 06 '23 at 13:30
41

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.

fredpi
  • 8,414
  • 5
  • 41
  • 61
Michael Peterson
  • 10,383
  • 3
  • 54
  • 51
  • 1
    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. – Robert Wagstaff Mar 23 '17 at 05:22
  • 3
    Don't forget to call `super.didMoveToSuperview()` – cicerocamargo Sep 28 '17 at 11:36
  • @cicerocamargo Apple says about `didMoveToSuperview`: "The default implementation of this method does nothing." – Ivan Smetanin Oct 17 '18 at 10:15
  • 4
    @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. – TNguyen Mar 06 '19 at 23:40
  • (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.) – Fattie Oct 10 '19 at 19:54
  • Is that supposed to be in TableView or in the Cell? – Julian Wagner Oct 27 '19 at 17:04
  • This ONLY helped me working great. The above all did nothing to me(using tableview cell in another tableview cell, Swift 4.0). – KoreanXcodeWorker May 18 '20 at 08:15
22

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.

Adrian P
  • 6,479
  • 4
  • 38
  • 55
Scenario
  • 296
  • 3
  • 6
  • 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? – Max Jun 18 '15 at 13:56
  • Thank you @ashy_32bit this solved it for me as well. – ImpurestClub Aug 18 '15 at 17:52
  • it would seem to be a plain bug, @Scenario right? horrible stuff. – Fattie Sep 23 '15 at 22:33
  • 3
    Using `[cell layoutSubviews]` instead of layoutIfNeeded might be possible fix. Refer http://stackoverflow.com/a/33515872/1474113 – ypresto Nov 13 '15 at 10:41
15

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;
}
Vitalii Gozhenko
  • 9,220
  • 2
  • 48
  • 66
  • 1
    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). – GK100 May 30 '16 at 18:34
  • 1
    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 – Cam Connor Jul 10 '20 at 06:46
13

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.

JAHelia
  • 6,934
  • 17
  • 74
  • 134
10

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

alvinmeimoun
  • 1,472
  • 1
  • 19
  • 38
  • 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 Nov 13 '17 at 10:05
9

I have tried most of the answers to this question and could not get any of them to work. The only functional solution I found was to add the following to my UITableViewController subclass:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    UIView.performWithoutAnimation {
        tableView.beginUpdates()
        tableView.endUpdates()
    }
}

The UIView.performWithoutAnimation call is required, otherwise you will see the normal table view animation as the view controller loads.

Chris Vig
  • 8,552
  • 2
  • 27
  • 35
5

In my case the last line of the UILabel was truncated when the cell was displayed for the first time. It happened pretty randomly and the only way to size it correctly was to scroll the cell out of the view and to bring it back. I tried all the possible solutions displayed so far (layoutIfNeeded..reloadData) but nothing worked for me. The trick was to set "Autoshrink" to Minimuum Font Scale (0.5 for me). Give it a try

Claus
  • 5,662
  • 10
  • 77
  • 118
5

Add a constraint for all content within a table view custom cell, then estimate table view row height and set row hight to automatic dimension with in a viewdid load :

    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

To fix that initial loading issue apply layoutIfNeeded method with in a custom table view cell :

class CustomTableViewCell: UITableViewCell {

override func awakeFromNib() {
    super.awakeFromNib()
    self.layoutIfNeeded()
    // Initialization code
}
}
bikram sapkota
  • 1,106
  • 1
  • 13
  • 18
  • 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. – PJ_Finnegan Nov 13 '17 at 10:07
4

None of the above solutions worked but the following combination of the suggestions did.

Had to add the following in viewDidLoad().

DispatchQueue.main.async {

        self.tableView.reloadData()

        self.tableView.setNeedsLayout()
        self.tableView.layoutIfNeeded()

        self.tableView.reloadData()

    }

The above combination of reloadData, setNeedsLayout and layoutIfNeeded worked but not any other. Could be specific to the cells in the project though. And yes, had to invoke reloadData twice to make it work.

Also set the following in viewDidLoad

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight

In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)

cell.setNeedsLayout()
cell.layoutIfNeeded() 
  • My similar `reloadData/beginUpdates/endUpdates/reloadData` is also working; `reloadData` **must** be called a second time. Don't need to wrap it in `async`. – ray Jul 25 '18 at 00:36
  • 1
    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 – Zaheer Moola Jul 29 '21 at 23:12
4

calling cell.layoutIfNeeded() inside cellForRowAt worked for me on ios 10 and ios 11, but not on ios 9.

to get this work on ios 9 also, I call cell.layoutSubviews() and it did the trick.

axunic
  • 2,256
  • 18
  • 18
3

Setting preferredMaxLayoutWidth helps in my case. I added

cell.detailLabel.preferredMaxLayoutWidth = cell.frame.width

in my code.

Also refer to Single line text takes two lines in UILabel and http://openradar.appspot.com/17799811.

Community
  • 1
  • 1
Xhacker Liu
  • 1,578
  • 1
  • 16
  • 25
2

I tried all of the solutions in this page but unchecking use size classes then checking it again solved my problem.

Edit: Unchecking size classes causes a lot of problems on storyboard so I tried another solution. I populated my table view in my view controller's viewDidLoad and viewWillAppear methods. This solved my problem.

shim
  • 9,289
  • 12
  • 69
  • 108
ACengiz
  • 1,285
  • 17
  • 23
2

Attatching screenshot for your referanceFor me none of these approaches worked, but I discovered that the label had an explicit Preferred Width set in Interface Builder. Removing that (unchecking "Explicit") and then using UITableViewAutomaticDimension worked as expected.

SARATH SASI
  • 1,395
  • 1
  • 15
  • 39
Jack James
  • 5,252
  • 6
  • 36
  • 38
2

In my case, a stack view in the cell was causing the problem. It's a bug apparently. Once I removed it, the problem was solved.

2

For iOS 12+ only, 2019 onwards...

An ongoing example of Apple's occasional bizarre incompetence, where problems go on for literally years.

It does seem to be the case that

        cell.layoutIfNeeded()
        return cell

will fix it. (You're losing some performance of course.)

Such is life with Apple.

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
1

I have the issue with resizing label so I nee just to do
chatTextLabel.text = chatMessage.message chatTextLabel?.updateConstraints() after setting up the text

// full code

func setContent() {
    chatTextLabel.text = chatMessage.message
    chatTextLabel?.updateConstraints()

    let labelTextWidth = (chatTextLabel?.intrinsicContentSize().width) ?? 0
    let labelTextHeight = chatTextLabel?.intrinsicContentSize().height

    guard labelTextWidth < originWidth && labelTextHeight <= singleLineRowheight else {
      trailingConstraint?.constant = trailingConstant
      return
    }
    trailingConstraint?.constant = trailingConstant + (originWidth - labelTextWidth)

  }
Svitlana
  • 2,938
  • 1
  • 29
  • 38
1

In my case, I was updating in other cycle. So tableViewCell height was updated after labelText was set. I deleted async block.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
     let cell = tableView.dequeueReusableCell(withIdentifier:Identifier, for: indexPath) 
     // Check your cycle if update cycle is same or not
     // DispatchQueue.main.async {
        cell.label.text = nil
     // }
}
Den Jo
  • 413
  • 4
  • 10
1

Just make sure you're not setting the label text in 'willdisplaycell' delegate method of table view. Set the label text in 'cellForRowAtindexPath' delegate method for dynamic height calculation.

You're Welcome :)

1

The problem is that the initial cells load before we have a valid row height. The workaround is to force a table reload when the view appears.

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self.tableView reloadData];
}
ali ozkara
  • 5,425
  • 2
  • 27
  • 24
  • 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 – TheTravloper Jan 11 '19 at 04:01
0

In my case, the issue with the cell height takes place after the initial table view is loaded, and a user action takes place (tapping on a button in a cell that has an effect of changing the cell height). I have been unable to get the cell to change its height unless I do:

[self.tableView reloadData];

I did try

[cell layoutIfNeeded];

but that didn't work.

Chris Prince
  • 7,288
  • 2
  • 48
  • 66
0

In Swift 3. I had to call self.layoutIfNeeded() each time I update the text of the reusable cell.

import UIKit
import SnapKit

class CommentTableViewCell: UITableViewCell {

    static let reuseIdentifier = "CommentTableViewCell"

    var comment: Comment! {
        didSet {
            textLbl.attributedText = comment.attributedTextToDisplay()
            self.layoutIfNeeded() //This is a fix to make propper automatic dimentions (height).
        }
    }

    internal var textLbl = UILabel()

    override func layoutSubviews() {
        super.layoutSubviews()

        if textLbl.superview == nil {
            textLbl.numberOfLines = 0
            textLbl.lineBreakMode = .byWordWrapping
            self.contentView.addSubview(textLbl)
            textLbl.snp.makeConstraints({ (make) in
                make.left.equalTo(contentView.snp.left).inset(10)
                make.right.equalTo(contentView.snp.right).inset(10)
                make.top.equalTo(contentView.snp.top).inset(10)
                make.bottom.equalTo(contentView.snp.bottom).inset(10)
            })
        }
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let comment = comments[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: CommentTableViewCell.reuseIdentifier, for: indexPath) as! CommentTableViewCell
        cell.selectionStyle = .none
        cell.comment = comment
        return cell
    }

commentsTableView.rowHeight = UITableViewAutomaticDimension
    commentsTableView.estimatedRowHeight = 140
Naloiko Eugene
  • 2,453
  • 1
  • 28
  • 18
0

I ran into this issue and fixed it by moving my view/label initialization code FROM tableView(willDisplay cell:) TO tableView(cellForRowAt:).

raisedandglazed
  • 804
  • 11
  • 29
  • 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 Oct 11 '17 at 14:41
  • 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`. – Martin Mar 23 '20 at 08:12
0

I found a pretty good workaround for this. Since the heights cannot be calculated before the cell is visible, all you need to do is scroll to the cell before calculating it's size.

tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
let cell = tableView.cellForRow(at: indexPath) ?? UITableViewCell()
let size = cell.systemLayoutSizeFitting(CGSize(width: frame.size.width, height: UIView.layoutFittingCompressedSize.height))
hundreth
  • 841
  • 4
  • 8
0

iOS 11+

table views use estimated heights by default. This means that the contentSize is just as estimated value initially. If you need to use the contentSize, you’ll want to disable estimated heights by setting the 3 estimated height properties to zero: tableView.estimatedRowHeight = 0 tableView.estimatedSectionHeaderHeight = 0 tableView.estimatedSectionFooterHeight = 0

    public func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
        return CGFloat.leastNormalMagnitude
    }

    public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return CGFloat.leastNormalMagnitude
    }

    public func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat {
        return CGFloat.leastNormalMagnitude
    }
Binoy jose
  • 461
  • 4
  • 9
0

Another solution

@IBOutlet private weak var tableView: UITableView! {
    didSet {
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = UITableView.automaticDimension
    }
}

extension YourViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        UITableView.automaticDimension
    }
}

Then you don't need to layoutSabviews

Oleksii
  • 53
  • 1
  • 6
0

override the prepareForReuse() and set lable to nil in you cell class

override func prepareForReuse() {
        super.prepareForReuse()
        self.detailLabel.text = nil
        self.titleLabel.text = nil
    }
Shairjeel ahmed
  • 51
  • 2
  • 11
0

Quick and dirty way. Double reloadData like that:

tableView.reloadData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { [weak self] in
   self?.tableView.reloadData()
})
Medhi
  • 2,656
  • 23
  • 16
0

Just make sure you are not engaging the main thread too much

ie : Don't use DispatchQueue.main.async in TableViewCell or to set the values... This will engage the main thread and mess up the height of the cell doesn't matter if you use all the above solutions.

Wahab Khan Jadon
  • 875
  • 13
  • 21
-1
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{


//  call the method dynamiclabelHeightForText
}

use the above method which return the height for the row dynamically. And assign the same dynamic height to the the lable you are using.

-(int)dynamiclabelHeightForText:(NSString *)text :(int)width :(UIFont *)font
{

    CGSize maximumLabelSize = CGSizeMake(width,2500);

    CGSize expectedLabelSize = [text sizeWithFont:font
                                constrainedToSize:maximumLabelSize
                                    lineBreakMode:NSLineBreakByWordWrapping];


    return expectedLabelSize.height;


}

This code helps you finding the dynamic height for text displaying in the label.

Adrian P
  • 6,479
  • 4
  • 38
  • 55
Ramesh Muthe
  • 811
  • 7
  • 15
  • 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. – BonanzaDriver Jan 15 '15 at 22:39