ChatGPT解决这个技术问题 Extra ChatGPT

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.

https://i.stack.imgur.com/2eKHx.png

I am also facing this issue,But the below answer is not working for me,Any help on this
facing the same issue adding layoutIfNeeded for cell doesn't work me as well ? any more suggestions
Great spot. This is still a problem in 2019 :/
I found what helped me in the following link - it's a pretty comprehensive discussion of variable height table view cells: stackoverflow.com/a/18746930/826946

r
rintaro

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

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.
Worked for me! And I would love to know why is it required!
Did not work if you render size class, that different from any X any
@AndreyKonstantinov Yup it doesn't work with size class, how do I make it work with size class?
It works without size class, any solution with size class?
f
fredpi

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.


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.
Don't forget to call super.didMoveToSuperview()
@cicerocamargo Apple says about didMoveToSuperview: "The default implementation of this method does nothing."
@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.
(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.)
A
Adrian P

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.


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?
Thank you @ashy_32bit this solved it for me as well.
it would seem to be a plain bug, @Scenario right? horrible stuff.
Using [cell layoutSubviews] instead of layoutIfNeeded might be possible fix. Refer stackoverflow.com/a/33515872/1474113
V
Vitalii Gozhenko

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

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).
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
J
JAHelia

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.


For swift 5 this worked even in collection view, god bless you, bro.
For me, this returned the correct height and layout of the cell, but it had no data in it
Try setNeedsDisplay() after those lines
@JAHelia Thank you brother, it works !!!
a
alvinmeimoun

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


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.
C
Chris Vig

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.


working and definitely better than invoking reloadData twice
Black magic. Doing it in viewWillAppear did not work for me, but doing it in viewDidAppear did.
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.
amazing i have tried alot of examples but fails you are demon
Same as @martin
C
Claus

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


this worked for me, without applying any of the other solutions listed above. Great find.
But this autoshrinks the text... why would one want to have different sizes of text in a table view? Looks terrible.
b
bikram sapkota

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

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.
C
Community

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.


J
Jimmy George Thomas

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.
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
a
axumnemonic

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.


G
Guilherme Carvalho

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.


i tried all others solution, ony your solution worked for thank you for sharing it
S
SARATH SASI

https://i.stack.imgur.com/IPjfo.png


s
shim

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.


S
Svitlana

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)

  }

D
Den Jo

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

I also have an async block but it is needed? Is there no alternative?
P
Pranav Rivankar

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 :)


A
Ali Ozkara

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

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
C
Community

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.


C
Chris Prince

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.


N
Naloiko Eugene

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

r
raisedandglazed

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


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.
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.
h
hundreth

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))

B
Binoy jose

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
    }

O
Oleksii

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


S
Shairjeel ahmed

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
    }

M
Medhi

Quick and dirty way. Double reloadData like that:

tableView.reloadData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { [weak self] in
   self?.tableView.reloadData()
})

W
Wahab Khan Jadon

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.


A
Adrian P
- (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.


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.