繁体   English   中英

UITableView 动态单元格高度仅在滚动后才正确

[英]UITableView dynamic cell heights only correct after some scrolling

我有一个UITableView一个自定义UITableViewCell在故事板使用自动布局定义。 单元格有几个多行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;
}

有什么我可能会遗漏的,导致自动布局在前几次无法完成工作吗?

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

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

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

当其他类似的解决方案没有时,这对我有用:

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

这似乎是一个实际的错误,因为我非常熟悉 AutoLayout 以及如何使用 UITableViewAutomaticDimension,但是我仍然偶尔会遇到这个问题。 我很高兴我终于找到了一些可以作为解决方法的东西。

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

也没有以[cell setNeedsLayout]开头。

您仍然需要滚动某些单元格并返回到视图中才能正确调整大小。

这非常令人沮丧,因为大多数开发人员都有动态类型、自动布局和自适应单元格正常工作——除了这个烦人的情况。 这个错误影响了我所有的“更高”的表视图控制器。

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

为什么会发生?

在 Storyboard 中设计的单元格为某些设备具有一定的宽度。 例如 400 像素。 例如,您的标签具有相同的宽度。 当它从故事板加载时,它的宽度为 400 像素。

这里有一个问题:

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

所以它计算了宽度为 400px 的标签和单元格的高度。 但是您在带有屏幕的设备上运行,例如 320px。 而这个自动计算的高度是不正确的。 仅仅因为单元格的layoutSubviews仅在tableView:heightForRowAtIndexPath:之后发生,即使您在layoutSubviews手动为标签设置preferredMaxLayoutWidth layoutSubviews

我的解决方案:

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

我有一个类似的问题,在第一次加载时,没有计算行高,但在滚动或转到另一个屏幕后,我回到这个屏幕行被计算。 在第一次加载时,我的项目是从 Internet 加载的,在第二次加载时,我的项目首先从 Core Data 加载并从 Internet 重新加载,我注意到行高是在从 Internet 重新加载时计算的。 所以我注意到在转场动画期间调用 tableView.reloadData() 时(推送和呈现转场的问题相同),没有计算行高。 所以我在视图初始化时隐藏了 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()
    })

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

上述解决方案都不适合我,有效的是这个魔法秘诀:按以下顺序调用它们:

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

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

我已经尝试了这个问题的大部分答案,但无法让其中任何一个起作用。 我发现的唯一功能解决方案是将以下内容添加到我的UITableViewController子类中:

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

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

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

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

    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

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

class CustomTableViewCell: UITableViewCell {

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

设置preferredMaxLayoutWidth对我有帮助。 我加了

cell.detailLabel.preferredMaxLayoutWidth = cell.frame.width

在我的代码中。

另请参阅UILabelhttp://openradar.appspot.com/17799811 中的单行文本需要两行

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

为了在 ios 9 上也能完成这项工作,我调用了cell.layoutSubviews()并且它成功了。

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

编辑:取消选中大小类会导致故事板上出现很多问题,因此我尝试了另一种解决方案。 我在我的视图控制器的viewDidLoadviewWillAppear方法中填充了我的表视图。 这解决了我的问题。

附上截图供您参考 对我来说,这些方法都不起作用,但我发现标签在 Interface Builder 中设置了明确的Preferred Width 删除(取消选中“显式”)然后使用UITableViewAutomaticDimension按预期工作。

上述解决方案均无效,但以下建议组合有效。

必须在 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() 

就我而言,单元格中的堆栈视图导致了问题。 这显然是一个错误。 一旦我删除它,问题就解决了。

我有调整标签大小的问题,所以我只需要做
chatTextLabel.text = chatMessage.message chatTextLabel?.updateConstraints() 设置好文字后

// 完整代码

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)

  }

就我而言,我在其他周期进行更新。 所以 tableViewCell 高度在 labelText 设置后更新。 我删除了异步块。

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

只要确保您没有在表格视图的 'willdisplaycell' 委托方法中设置标签文本。 在 'cellForRowAtindexPath' 委托方法中设置标签文本以进行动态高度计算。

别客气 :)

问题是初始单元格在我们获得有效行高之前加载。 解决方法是在视图出现时强制重新加载表。

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

仅适用于 iOS 12+,2019 年以后...

苹果偶尔出现奇怪的无能的一个持续例子,问题实际上持续了数年。

情况似乎确实如此

        cell.layoutIfNeeded()
        return cell

将修复它。 (当然,您会失去一些性能。)

这就是苹果的生活。

在我的情况下,单元格高度的问题发生在加载初始表格视图之后,并且发生了用户操作(点击单元格中具有更改单元格高度效果的按钮)。 除非我这样做,否则我一直无法让单元格改变其高度:

[self.tableView reloadData];

我确实尝试过

[cell layoutIfNeeded];

但这没有用。

在 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

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

对于仍然面临此问题的人,请尝试这条神奇的线条

  tableview.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)

只需确保在填充表后再调用数据,这样就可以确保第0部分和第0行始终可用,否则它会崩溃,你可以先检查它们的可用性,希望这会有所帮助。

我找到了一个很好的解决方法。 由于在单元格可见之前无法计算高度,因此您需要做的就是在计算单元格大小之前滚动到单元格。

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 个估计高度属性设置为零来禁用估计高度: 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
    }

另一种解决方案

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

那你就不需要layoutSabviews了

覆盖 prepareForReuse() 并将标签设置为 nil 在您的单元格类中

override func prepareForReuse() {
        super.prepareForReuse()
        self.detailLabel.text = nil
        self.titleLabel.text = nil
    }
- (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;


}

此代码可帮助您找到标签中显示的文本的动态高度。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM