UITableView 动态单元格高度只有在一些滚动之后才会正确

我有一个使用自动布局在故事板中定义的自定义 UITableViewCellUITableView。该细胞有几个多行 UILabels

UITableView似乎可以正确地计算单元格高度,但是对于前几个单元格,高度在标签之间没有被正确地划分。 稍微滚动一下之后,一切都按预期运行(甚至包括最初不正确的单元格)。

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

有没有什么我可能错过了,这是导致自动布局不能做它的工作的头几次?

我创建了一个 样本项目来演示这种行为。

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

72965 次浏览

我不知道这是否有明确的文档说明,但是在返回单元格之前添加 [cell layoutIfNeeded]可以解决问题。

- (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;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{




//  call the method dynamiclabelHeightForText
}

使用上面的方法动态返回行的高度。 并为您正在使用的标签分配相同的动态高度。

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




}

此代码有助于查找标签中显示的文本的动态高度。

在我的例子中,当单元格第一次显示时,UILabel 的最后一行被截断。这种情况发生得非常随机,唯一正确调整大小的方法就是将单元格滚动到视图之外,然后将其返回。 我尝试了目前为止显示的所有可能的解决方案(layoutIfNeeded)。.ReloadData) ,但是没有一个对我有用。诀窍是将 “自动收缩”设置为 最小字体比例(对我来说是0.5)。试试看

cellForRowAtIndexPath中添加 [cell layoutIfNeeded]对于最初滚动到视图之外的单元格不起作用。

[cell setNeedsLayout]作序也不行。

您仍然需要将某些单元格向外滚动并返回到视图中,以便正确调整其大小。

这是相当令人沮丧的,因为大多数开发人员有动态类型,自动布局和自我大小的单元格正常工作ーー除了这个恼人的情况。这个 bug 会影响我所有的“高”表视图控制器。

在我的情况下,设置 preferredMaxLayoutWidth有所帮助。 我加了一句

cell.detailLabel.preferredMaxLayoutWidth = cell.frame.width

在我的代码里。

也请参阅 单行文本在 UILabel 中有两行http://openradar.appspot.com/17799811

我尝试了这个页面中的所有解决方案,但不检查使用大小类,然后再次检查它解决了我的问题。

编辑: 不检查大小类会在故事板上引起很多问题,所以我尝试了另一种解决方案。我在视图控制器的 viewDidLoadviewWillAppear方法中填充了表视图。这解决了我的问题。

我在我的一个项目中也有过同样的经历。

为什么会这样?

在情节串连板中设计的单元为某些设备具有一定的宽度。例如400px。例如,您的标签具有相同的宽度。当它从故事板加载时,它的宽度是400px。

这里有一个问题:

tableView:heightForRowAtIndexPath:在单元格布局之前调用它的子视图。

因此,它计算的高度标签和单元格的宽度为400px。但是你运行在屏幕设备上,例如,320px。这个自动计算的高度是不正确的。因为细胞的 layoutSubviews只发生在 tableView:heightForRowAtIndexPath:之后 即使在 layoutSubviews中手动为标签设置 preferredMaxLayoutWidth,也无济于事。

我的解决办法是:

1)子类 UITableView并覆盖 dequeueReusableCellWithIdentifier:forIndexPath:。设置单元格宽度等于表格宽度并强制单元格的布局。

- (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) UITableViewCell分类。 为 layoutSubviews中的标签手动设置 preferredMaxLayoutWidth。你还需要手动布局 contentView,因为它不会自动布局后单元格帧改变(我不知道为什么,但它是)

- (void)layoutSubviews {
[super layoutSubviews];
[self.contentView layoutIfNeeded];
self.yourLongTextLabel.preferredMaxLayoutWidth = self.yourLongTextLabel.width;
}

我有问题与调整标签,所以我需要只是做
Text = chatMessage.message 在设置文本之后使用 chatTextLabel? . updateConstraint ()

//完整代码

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)


}

为表视图自定义单元格中的所有内容添加一个约束,然后估计表视图行高,并将行高设置为自动维度,在视图加载中:

    override func viewDidLoad() {
super.viewDidLoad()


tableView.estimatedRowHeight = 70
tableView.rowHeight = UITableViewAutomaticDimension
}

要修复初始加载问题,请在自定义表视图单元格中应用 layoutIfNeeded 方法:

class CustomTableViewCell: UITableViewCell {


override func awakeFromNib() {
super.awakeFromNib()
self.layoutIfNeeded()
// Initialization code
}
}

在我的示例中,单元格高度的问题发生在加载了初始表视图之后,并且发生了用户操作(点击单元格中的按钮可以改变单元格高度)。我无法让细胞改变它的高度,除非我这样做:

[self.tableView reloadData];

我试过了

[cell layoutIfNeeded];

但没成功。

当其他类似的解决方案不起作用时,这对我有效:

override func didMoveToSuperview() {
super.didMoveToSuperview()
layoutIfNeeded()
}

这似乎是一个实际的错误,因为我非常熟悉自动布局和如何使用 UITableViewAutomatic維度,但我仍然偶尔遇到这个问题。我很高兴我终于找到了解决办法。

我有一个类似的问题,在第一次加载时,行高没有计算,但经过一些滚动或转到另一个屏幕,我回到这个屏幕行都是计算好的。在第一次加载我的项目是从互联网加载,在第二次加载我的项目是从核心数据首先加载和重新加载从互联网,我注意到行高度是计算从互联网重新加载。因此,我注意到当在 segue 动画期间调用 tableView.reloadData ()时(推送和现在 segue 存在同样的问题) ,没有计算行高。因此,我在视图初始化时隐藏了 tableview,并放置了一个活动加载器以防止对用户造成不良影响,在300ms 后调用 tableView.reloadData,现在问题解决了。我认为这是一个 UIKit 的错误,但这个变通方案的把戏。

我把这些行(Swift 3.0)放在我的项目加载完成处理程序中

DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300), execute: {
self.tableView.isHidden = false
self.loader.stopAnimating()
self.tableView.reloadData()
})

这就解释了为什么有些人把 reloadData 放在 layoutSubviews 中来解决这个问题

在 Swift 3中,每次更新可重用单元格的文本时,我都必须调用 self. layoutIfNeeded ()。

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

Attatching screenshot for your referance对我来说,这些方法都不管用,但是我发现这个标签在 Interface Builder 中有一个明确的 Preferred Width设置。去掉这个选项(取消“显式”选项) ,然后使用 UITableViewAutomaticDimension,正如预期的那样。

在我的情况下,我在其他周期更新。因此,在 labelText 设置后更新了 tableViewCell 高度。我删除了异步块。

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

以上所有的解决方案都不起作用,但是下面的建议组合起作用了。

必须在 viewDidLoad ()中添加以下内容。

DispatchQueue.main.async {


self.tableView.reloadData()


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


self.tableView.reloadData()


}

上述 reloadData、 setNeedsLayout 和 layoutIfNeeded 的组合起作用了,但没有其他任何组合。但是可以特定于项目中的单元格。是的,必须调用 reloadData 两次才能正常工作。

还可以在 viewDidLoad 中设置以下内容

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight

在 tableView (_ tableView: UITableView,cellForRowAt indexPath: IndexPath)中

cell.setNeedsLayout()
cell.layoutIfNeeded()

只要确保没有在表视图的“ will displaycell”委托方法中设置标签文本即可。 在“ cellForRowAtindexPath”委托方法中设置用于动态高度计算的标签文本。

不客气

在我的例子中,单元格中的堆栈视图导致了这个问题。显然是个窃听器。一旦我把它取出来,问题就解决了。

这个问题的大部分答案我都试过了,但是没有一个有效。我找到的唯一功能性解决方案是在我的 UITableViewController子类中添加以下内容:

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

UIView.performWithoutAnimation调用是必需的,否则在视图控制器加载时您将看到普通的表视图动画。

我遇到了这个问题,并通过将视图/标签初始化代码从 tableView(willDisplay cell:)移动到 tableView(cellForRowAt:)来修复它。

问题是,在有效的行高之前加载初始单元格。 解决方案是在视图出现时强制重新加载表。

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.tableView reloadData];
}

cellForRowAt内部调用 cell.layoutIfNeeded()在 ios 10和 ios 11上对我有用,但在 ios 9上没有。

为了在 ios 9上完成这个工作,我调用了 cell.layoutSubviews(),它完成了这个任务。

以上的方法对我来说都不管用,真正管用的是这个魔法配方: 按这个顺序打电话给他们:

ReloadData ()
LayoutIfNeeded () StartUpdate () EndUpdate ()

我的 tableView 数据是从一个 Web 服务中填充的,在连接的回调中我写了上面的代码行。

对于 iOS12以上版本,2019年以后..。

这是苹果公司偶尔奇怪的无能的一个持续的例子,这些问题会持续好几年。

看起来的确是这样

        cell.layoutIfNeeded()
return cell

会修好的。(你当然会失去一些表现。)

这就是苹果的生活。

我找到了一个不错的解决办法。由于在单元格可见之前无法计算高度,所以您只需在计算单元格的大小之前滚动到该单元格即可。

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

IOS 11 +

默认情况下,表视图使用估计高度。这意味着 contentSize 与最初的估计值一样。如果需要使用 contentSize,则需要通过将3个估计高度属性设置为0来禁用估计高度: tableView.EstiatedRowHeight = 0 tableView.EstiatedSectionHeaderHeight = 0 tableView.EstiatedSectionFooterHeight = 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
}

另一个解决办法

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

那么你就不需要布局 Sabviews 了

在单元格类中覆盖 prereForReuse ()并将 lable 设置为 nil

override func prepareForReuse() {
super.prepareForReuse()
self.detailLabel.text = nil
self.titleLabel.text = nil
}

快速和肮脏的方法。双重重载数据如下:

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

只要确保你没有参与 主线太多

即: 不要在 TableViewCell中使用 DispatchQueue.main.async或设置值... 如果使用上述所有解决方案,这将使主线程参与进来,并且搞乱单元格的高度并不重要。