UITableView dynamic cell heights only correct after some scrolling

IosUitableviewIos8AutolayoutIos Autolayout

Ios Problem Overview


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.

Ios Solutions


Solution 1 - Ios

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

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

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

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

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.

Solution 6 - Ios

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

Solution 7 - Ios

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.

Solution 8 - Ios

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

Solution 9 - Ios

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

Solution 10 - Ios

Setting preferredMaxLayoutWidth helps in my case. I added

cell.detailLabel.preferredMaxLayoutWidth = cell.frame.width

in my code.

Also refer to https://stackoverflow.com/questions/26840137/single-line-text-takes-two-lines-in-uilabel/26840677 and http://openradar.appspot.com/17799811.

Solution 11 - Ios

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

Solution 12 - Ios

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.

Solution 13 - Ios

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.

Solution 14 - Ios

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.

Solution 15 - Ios

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.

Solution 16 - Ios

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)

  }

Solution 17 - Ios

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

Solution 18 - Ios

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

Solution 19 - Ios

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

Solution 20 - Ios

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.

Solution 21 - Ios

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.

Solution 22 - Ios

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

Solution 23 - Ios

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

Solution 24 - Ios

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

Solution 25 - Ios

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
    }

Solution 26 - Ios

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

Solution 27 - Ios

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
	}

Solution 28 - Ios

Quick and dirty way. Double reloadData like that:

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

Solution 29 - Ios

- (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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionblackpView Question on Stackoverflow
Solution 1 - IosrintaroView Answer on Stackoverflow
Solution 2 - IosMichael PetersonView Answer on Stackoverflow
Solution 3 - IosScenarioView Answer on Stackoverflow
Solution 4 - IosVitalii GozhenkoView Answer on Stackoverflow
Solution 5 - IosJAHeliaView Answer on Stackoverflow
Solution 6 - IosalvinmeimounView Answer on Stackoverflow
Solution 7 - IosChris VigView Answer on Stackoverflow
Solution 8 - IosClausView Answer on Stackoverflow
Solution 9 - Iosbikram sapkotaView Answer on Stackoverflow
Solution 10 - IosXhacker LiuView Answer on Stackoverflow
Solution 11 - IosJimmy George ThomasView Answer on Stackoverflow
Solution 12 - IosaxumnemonicView Answer on Stackoverflow
Solution 13 - IosACengizView Answer on Stackoverflow
Solution 14 - IosJack JamesView Answer on Stackoverflow
Solution 15 - IosGuilherme CarvalhoView Answer on Stackoverflow
Solution 16 - IosSvitlanaView Answer on Stackoverflow
Solution 17 - IosDen JoView Answer on Stackoverflow
Solution 18 - IosPranav RivankarView Answer on Stackoverflow
Solution 19 - IosAli OzkaraView Answer on Stackoverflow
Solution 20 - IosFattieView Answer on Stackoverflow
Solution 21 - IosChris PrinceView Answer on Stackoverflow
Solution 22 - IosNaloiko EugeneView Answer on Stackoverflow
Solution 23 - IosraisedandglazedView Answer on Stackoverflow
Solution 24 - IoshundrethView Answer on Stackoverflow
Solution 25 - IosBinoy joseView Answer on Stackoverflow
Solution 26 - IosOleksiiView Answer on Stackoverflow
Solution 27 - IosShairjeel ahmedView Answer on Stackoverflow
Solution 28 - IosMedhiView Answer on Stackoverflow
Solution 29 - IosRamesh MutheView Answer on Stackoverflow