UITableView动态单元格高度只有在滚动一段时间后才正确

132

我有一个带有自定义UITableViewCellUITableView,它在storyboard中使用自动布局进行定义。该单元格具有多个多行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;
}

我可能忽略了什么,导致自动布局在前几次无法正常工作吗?

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

示例项目:第一次加载时表格视图的顶部。 示例项目:在向下滚动和向上滚动后相同的单元格。


1
我也遇到了这个问题,但下面的答案对我没有用,有什么帮助吗? - Shangari C
1
遇到相同的问题,为单元格添加layoutIfNeeded也无法解决。还有其他建议吗? - Max
2
很好的发现。这在2019年仍然是一个问题 :/ - Fattie
我在以下链接中找到了帮助我的内容 - 这是一个非常全面的变量高度表视图单元讨论:https://dev59.com/4WMl5IYBdhLWcg3wCDGW#18746930 - Andy Weinstein
30个回答

155
我不知道这是否有明确的文档记录,但在返回单元格之前添加[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;
}

3
不过,我想知道为什么只有在第一次使用时才需要。如果我在单元格的-awakeFromNib中调用-layoutIfNeeded,它仍然能够正常工作。我更愿意在我知道它必要的地方才调用 layoutIfNeeded - blackp
3
做得对!我想知道为什么这是必需的! - Mustafa
如果您渲染的大小类与任何 X 任何不同,则无法工作。 - Andrei Konstantinov
@AndreyKonstantinov 嗯,它不支持尺寸类,我该如何让它支持尺寸类? - z22
它可以在没有尺寸类的情况下工作,有任何使用尺寸类的解决方案吗? - Yuvrajsinh
显示剩余5条评论

41

在其他类似的解决方案都无法解决问题时,这个方法对我起了作用:

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

看起来这像是一个实际的bug,因为我非常熟悉AutoLayout和如何使用UITableViewAutomaticDimension,但是我偶尔仍然会遇到这个问题。我很高兴终于找到了一些可行的解决办法。


1
这个答案比被接受的回答更好,因为它只在从xib加载单元格时调用,而不是每次显示单元格都调用。而且代码行数也少得多。 - Robert Wagstaff
3
别忘了调用 super.didMoveToSuperview() - cicerocamargo
5
即使默认实现什么也不做,调用父类方法仍然很重要,因为在将来它可能会执行某些操作。 - TNguyen
这应该放在TableView还是Cell中? - Julian Wagner
这真的帮了我很多,其他方法对我都没有用(在另一个tableview cell中使用tableview cell,Swift 4.0)。 - KoreanXcodeWorker
显示剩余2条评论

22

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

即使在其前面加上[cell setNeedsLayout]也不行。

您仍然需要将某些单元格滚动到视图内部并再次滚动出来,以使它们正确地调整大小。

这非常令人沮丧,因为大多数开发人员都已经成功使用Dynamic Type、AutoLayout和Self-Sizing Cells —— 除了这种恼人的情况。这个错误会影响我所有“更高”table view控制器。


我也有同样的问题。当我第一次滚动时,单元格的大小为44像素。如果我滚动使单元格不可见,然后再回来,它的大小就正确了。你找到任何解决方案了吗? - Max
谢谢 @ashy_32bit,这对我也解决了问题。 - ImpurestClub
这似乎是一个普通的错误,@Scenario 对吧?太可怕了。 - Fattie
3
使用[cell layoutSubviews]代替layoutIfNeeded可能是一种可行的解决方法。请参考https://dev59.com/kF8e5IYBdhLWcg3wfafv#33515872。 - ypresto

15

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

为什么会发生这种情况呢?

在Storyboard中设计的单元格具有某些设备的宽度,例如400像素。例如,您的标签具有相同的宽度。当它从Storyboard中加载时,它的宽度为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;
}

1
当我遇到这个问题时,我还需要确保tableview的框架是正确的,所以我在tableview被填充之前(在viewDidLoad中)调用了[self.tableview layoutIfNeeded]。 - GK100
1
我从未停下来思考它是否与宽度不正确有关。在cellForRowAt函数中,cell.frame.size.width = tableview.frame.width然后cell.layoutIfNeeded()为我解决了这个问题。 - Cam Connor

13

我尝试了以上的所有解决方案,但都没有奏效。接下来我用了这个神奇的方法:

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

我的tableView数据是从Web服务获取的,在连接的回调中,我加入了以上这几行代码。


1
对于Swift 5,即使在集合视图中也可以工作,上帝保佑你,兄弟。 - Govani Dhruv Vijaykumar
对我来说,这返回了单元格的正确高度和布局,但其中没有数据。 - Darrow Hartman
在那些代码行之后尝试使用setNeedsDisplay()函数。 - JAHelia

10

我有一个类似的问题,第一次加载时,行高没有计算,但在滚动一些或进入另一个屏幕并返回到该屏幕后,行高会被计算。第一次加载我的项目是从互联网上加载的,第二次加载我的项目首先从Core Data中加载,然后重新从互联网加载,我注意到行高是在重新从互联网加载时被计算的。因此,我发现当tableView.reloadData()在segue动画期间调用时(push和present segue也存在同样的问题),行高没有计算。因此,在视图初始化时隐藏了tableView并放置了一个活动加载器以防止对用户造成不良影响,并在300毫秒后调用tableView.reloadData,现在问题已解决。我认为这是UIKit的一个bug,但这个解决方法很有效。

我将下面这些代码(Swift 3.0)放在我的项目加载完成处理程序中:

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

这就解释了为什么对于某些人来说,在layoutSubviews中放置一个reloadData可以解决问题。


这是唯一对我有效的WA。除了我不使用isHidden,而是在添加数据源时将表格的alpha设置为0,然后在重新加载后设置为1。 - PJ_Finnegan

9
我尝试了这个问题的大部分答案,但是它们都不能工作。我找到的唯一有效的解决方法是将以下内容添加到我的UITableViewController子类中:
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    UIView.performWithoutAnimation {
        tableView.beginUpdates()
        tableView.endUpdates()
    }
}
UIView.performWithoutAnimation 调用是必需的,否则在视图控制器加载时会看到普通的表格视图动画。

工作起来肯定比调用reloadData两次要好 - Jimmy George Thomas
4
黑魔法。在 viewWillAppear 中执行它对我没用,但在 viewDidAppear 中执行就可以了。 - Martin
当我在viewDidAppear中实现这个解决方案时,它确实对我起作用了。但从用户体验的角度来看,这绝不是最佳选择,因为随着正确的大小调整发生,表格内容会跳来跳去。 - richardpiazza
很棒,我尝试了很多例子但都失败了,你是魔鬼。 - Shakeel Ahmed
与@martin相同 - A.J. Hernandez

5
在表视图自定义单元格中为所有内容添加约束,然后在viewDidLoad中估算表视图行高并设置行高为自动尺寸。
    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

为了解决初始加载问题,在自定义的表格视图单元格中应用layoutIfNeeded方法:
class CustomTableViewCell: UITableViewCell {

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

重要的是将estimatedRowHeight设置为大于0的值,而不是UITableViewAutomaticDimension(-1),否则自动行高度将无法正常工作。 - PJ_Finnegan

5
在我的情况下,当单元格第一次显示时,UILabel 的最后一行被截断了。它发生得非常随机,唯一正确调整大小的方法是将单元格滚动出视图并将其带回。我尝试了目前所提供的所有可能的解决方案(layoutIfNeeded..reloadData),但对我来说都无效。关键在于将“Autoshrink”设置为最小字体比例(对我来说是0.5)。你可以试试。

这对我起作用了,而且没有应用上面列出的任何其他解决方案。真是个好发现。 - mckeejm
2
但是这会自动缩小文本...为什么在表视图中要有不同大小的文本?看起来很糟糕。 - lucius degeer

4

在iOS 10和iOS 11上,在cellForRowAt中调用cell.layoutIfNeeded()对我有用,但在iOS 9上不起作用。

为了在iOS 9上也能实现此功能,我调用了cell.layoutSubviews(),这样就解决了问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接