UITableViewCell 的异步高度更改

Posted

技术标签:

【中文标题】UITableViewCell 的异步高度更改【英文标题】:Async height change for UITableViewCell 【发布时间】:2020-05-18 15:22:06 【问题描述】:

在我的一个项目中,我需要根据图像大小更改 UITableViewCell 中 UIImageView 的高度,但问题是有时我必须在单元格显示后才这样做。

所以,如果我事先知道所有图像大小,我当前的解决方案就像一个魅力,但如果我试图延迟计算它 - 它完全被破坏(特别是滚动但即使没有它它也会被破坏)。

我制作了示例项目来说明这一点。没有异步下载,但我试图在延迟(1s)后动态改变 UIImageView 的高度。高度取决于 UIImageView,因此下一个 UIImageView 应该比前一个略高(10 像素)。另外,我有一个 UILabel,仅限于 UIImageView。

看起来像这样(UIImageViews 是红色的)

如果我尝试异步执行此操作,它看起来像这样,所有的 UILabel 都在这里真的坏了。

这是滚动之后的一个(也是异步的):

我在这里做错了什么?我已经阅读了几个关于动态高度的主题,但没有一个解决方案对我有用。

我的代码相当简单:

func addTableView() 
    tableView = UITableView()
    tableView.translatesAutoresizingMaskIntoConstraints = false
    tableView.dataSource = self
    tableView.delegate = self
    tableView.separatorStyle = .none
    tableView.estimatedRowHeight = 100
    tableView.rowHeight = UITableView.automaticDimension
    tableView.backgroundColor = .black
    tableView.register(DynamicCell.self, forCellReuseIdentifier: "dynamicCell")
    view.addSubview(tableView)

    tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
    tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
    tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
    tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true



func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
    let cell = tableView.dequeueReusableCell(withIdentifier: "dynamicCell", for: indexPath) as! DynamicCell
        cell.message = messageArray[indexPath.row]
        cell.backgroundColor = .clear
        cell.selectionStyle = .none
        cell.buildCell()
    return cell

DynamicCell.swift(代表现在什么都不做):

var backView: UIView!
var label: UILabel!
var picView: UIImageView!

var message: DMessage?
var picViewHeight: NSLayoutConstraint!

var delegate: RefreshCellDelegate?

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) 
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    backView = UIView()
    backView.translatesAutoresizingMaskIntoConstraints = false
    backView.backgroundColor = .white
    backView.clipsToBounds = true
    backView.layer.cornerRadius = 8.0
    self.addSubview(backView)

    label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.textAlignment = .left
    label.textColor = .black
    label.numberOfLines = 0
    backView.addSubview(label)

    picView = UIImageView()
    picView.translatesAutoresizingMaskIntoConstraints = false
    picView.clipsToBounds = true
    picView.backgroundColor = .red
    backView.addSubview(picView)

    addMainConstraints()



func addMainConstraints() 
    backView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8).isActive = true
    backView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -32).isActive = true
    backView.topAnchor.constraint(equalTo: self.topAnchor, constant: 4).isActive = true
    backView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -4).isActive = true

    picView.topAnchor.constraint(equalTo: backView.topAnchor, constant: 0).isActive = true
    picView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 0).isActive = true
    picView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: 0).isActive = true

    label.topAnchor.constraint(equalTo: picView.bottomAnchor, constant: 0).isActive = true
    label.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 8).isActive = true
    label.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -8).isActive = true
    label.bottomAnchor.constraint(equalTo: backView.bottomAnchor, constant: -4).isActive = true

    picViewHeight = NSLayoutConstraint(item: picView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
    picViewHeight.priority = UILayoutPriority(999)
    picViewHeight.isActive = true



override func prepareForReuse() 
    picViewHeight.constant = 0
    //picViewHeight.constant = 0


func buildCell() 
    guard let message = message else return
    label.attributedText = NSAttributedString(string: message.text)
    changeHeightWithDelay()
    //changeHeightWithoutDelay()


func changeHeightWithoutDelay() 
    if let nh = self.message?.imageHeight 
        self.picViewHeight.constant = nh
        self.delegate?.refreshCell(cell: self)
    


func changeHeightWithDelay() 
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) 
        if let nh = self.message?.imageHeight 
            self.picViewHeight.constant = nh
            self.delegate?.refreshCell(cell: self)
        
    

【问题讨论】:

看来我可以通过实现 heightForRowAt 来解决部分甚至大部分问题,但我只想知道为什么目前的方法在这里似乎行不通。 我注意到一件事,当你在玩 cell 时,最好使用 contentView 而不是直接使用 self.即self.contentView.addSubview()。 refreshcell 功能有什么作用?您是否尝试将其标记为 needsSetDisplay 以便在下一个绘制周期中更新?你试过调用 layoutIfNeeded 吗? 我将假设您在 refreshCell 方法中使用 beginUpdatesendUpdates 来确保表格视图知道? @Joshua 感谢您的建议。我还没有尝试过 contentView,可以。现在refreshcell什么都不做,我试着玩setNeedsLayoutlayoutIfNeeded等等,但都无济于事。 @Rikh 我尝试在我的实际项目中使用这种方法,但似乎没有简单的方法可以使用beginUpdatesendUpdates,因为我的tableView 非常动态,随时可能有一个新元素,所以我有很多out of index 错误。 【参考方案1】:

将此作为答案。

我注意到一件事,当你在玩 cell 时,最好使用 contentView 而不是直接使用 self.即self.contentView.addSubview()。 refreshcell 功能有什么作用?您是否尝试将其标记为 needsSetDisplay 以便在下一个绘制周期中更新?你试过调用 layoutIfNeeded 吗?

为了进一步解释,当您想要更改视图的高度/宽度时,您的视图已经被“渲染”了,您需要通知它有更新。当您将视图标记为 setNeedsDisplay 并在下一个渲染周期中更新时会发生这种情况

更多关于苹果文档here

您可以使用此方法或 setNeedsDisplay(_:) 通知系统您的视图内容需要重绘。此方法记录请求并立即返回。直到下一个绘图周期才真正重绘视图,此时所有无效的视图都会更新。

【讨论】:

以上是关于UITableViewCell 的异步高度更改的主要内容,如果未能解决你的问题,请参考以下文章

计算取决于单元格宽度和异步加载的图像的 UITableViewCell 的高度

更改 UITableViewCell 内的 UIView 高度

在动态高度 UITableViewCell 出现后更改其高度

UITableViewCell 自定义类 - 更改子视图高度约束后重新加载单元格高度

根据视图更改自定义 UITableViewCell 高度

当 UITextField 获得焦点时,从 UITableViewCell 继承类中更改 UITableViewCell 的高度