如何为具有灵活高度和固定宽度的视图覆盖 intrinsictContentSize?

Posted

技术标签:

【中文标题】如何为具有灵活高度和固定宽度的视图覆盖 intrinsictContentSize?【英文标题】:How to override intrinsictContentSize for a view with flexible height and fixed width? 【发布时间】:2016-12-11 18:35:58 【问题描述】:

我正在尝试在 tableView 中显示自定义视图,并让 ios 使用 UITableViewAutomaticDimension 计算每一行的高度。不幸的是,我的牢房尺寸不合适(高度不好)。

我的自定义视图定义如下(这部分我没有使用AutoLayout,原因很多,我不想使用它):

class MyView : UIView 
    var label: UILabel!

    override init(frame: CGRect) 
        super.init(frame: frame)
        setupView()
    

    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        setupView()
    

    func setupView() 
        label = UILabel()
        label.numberOfLines = 0
        addSubview(label)
    

    func setText(text: String) 
        label.text = text
        setNeedsLayout()
    

    override func layoutSubviews() 
        super.layoutSubviews()
        label.frame = bounds
        invalidateIntrinsicContentSize()
    

    override var intrinsicContentSize: CGSize 
        guard let text = label.text else  return .zero 
        let width = bounds.width
        let height = text.heightWithConstrainedWidth(width: width, font: label.font)
        return CGSize(width: width, height: height)
    

现在,我将这个自定义视图包装在 UITableViewCell 中,以便在 UITableView 中使用它。这里我使用的是 AutoLayout:

class MyCell : UITableViewCell 
    var myView: MyView!

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) 
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupCell()
    

    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        setupCell()
    

    func setupCell() 
        myView = MyView()
        contentView.addSubview(myView)
        myView.snp.makeConstraints  make in
            make.edges.equalTo(contentView)
        
    

现在,使用下面的 viewController,我显示一个带有大文本的单元格,看看它的高度是否被自动计算:

class ViewController: UIViewController 
    var tableView: UITableView!

    override func viewDidAppear(_ animated: Bool) 
        super.viewDidAppear(animated)
        tableView = UITableView()
        view.addSubview(tableView)
        tableView.snp.makeConstraints  make in
            make.edges.equalTo(view)
        
        tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 30
        tableView.dataSource = self
    


extension ViewController: UITableViewDataSource 
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        return 1
    

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell") as! MyCell
        cell.myView.setText(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur")
        return cell
    

不幸的是,单元格的高度等于 21。即使最后一次调用 MyView 返回的高度是 143,但这是 143。这是结果的屏幕截图:

如果单元格被回收,则其大小正确(我将单元格滚动到屏幕外):

我知道MyViewintrinsictContentSize 使用它的框架并且它不应该使用它,但是我不知道如何设计一个在给定宽度的情况下具有灵活高度的 UIView 子类。考虑到我在 MyView 的 layoutSubviews() 中调用 invalidateIntrinsicContentSize(),我认为它会起作用。

如何更改此代码以从一开始就具有良好的单元格高度?

【问题讨论】:

你能详细说明你为什么选择不在这里使用自动布局吗?这正是它擅长的事情。 因为在我的真实用例中,我有其他东西而不是简单的 UILabel。我有一个由很多子视图组成的非常丰富的 UIView。如果我为所有这些都保留自动布局,我最终会在 tableViews 中遇到性能问题。 【参考方案1】:

文档表明覆盖 intrinsicContentSize 可能不是实现您想要实现的目标的最佳方式。正如您所观察到的,来自UIView 类参考:

设置此属性允许自定义视图根据其内容与布局系统通信它想要的大小。此固有尺寸必须独立于内容框架,因为例如,无法根据更改的高度将更改的宽度动态传达给布局系统。

MyView 的bounds 直到它的intrinsicContentSize 被请求之后才被设置,并且在自动布局上下文中不能保证在layoutSubviews() 被调用之前的任何时间都被设置。有一种方法可以解决这个问题,并将宽度传递给您的自定义视图以在intrinsicContentSize 中使用,但是,我已将其作为此答案中的第二个选项。

选项 1:通过覆盖 sizeThatFits(_:) 进行手动布局

首先,在没有自动布局的情况下,将 MyView 及其标签子视图配置为适当调整大小和大小:

//  MyView.swift

func setupView() 
    label = UILabel()
    label.numberOfLines = 0
    label.autoresizingMask = [.flexibleWidth] // New
    addSubview(label)


override func layoutSubviews() 
    super.layoutSubviews()
    label.sizeToFit() // New


override func sizeThatFits(_ size: CGSize) -> CGSize 
    return label.sizeThatFits(size) // New

MyView 现在将根据其标签子视图的大小调整自己的大小,并且标签将根据其父视图的宽度(因为它的 .flexibleWidth 自动调整大小掩码)和它的文本内容(因为 numberOfLines 是设置为 0)。如果 MyView 有其他子视图,则需要计算并返回 sizeThatFits(_:) 中的总大小。

其次,我们希望 UITableView 能够根据上面的手动布局计算其单元格和自定义子视图的高度。当调整单元格大小时,UITableView 在每个单元格上调用systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority),而后者又在单元格上调用sizeThatFits(_:)。 (见WWDC 2014 Session 226。)

传递给这些方法的目标大小或拟合大小是高度为零的表格视图的宽度。水平拟合优先级为UILayoutPriorityRequired。您可以通过覆盖自定义单元子类中的其中一种方法(前者用于自动布局,后者用于手动布局)并使用传入的大小的宽度来计算并返回细胞。

在这种情况下,单元格的大小是myView的大小:

//  MyCell.swift

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize 
    return myView.sizeThatFits(targetSize)

你已经完成了。

选项 2:通过覆盖 intrinsicContentSize 自动布局

由于 MyView 的大小取决于其标签的大小,因此第一步是确保标签的位置和大小正确。你已经这样做了:

//  MyView.swift

override func layoutSubviews() 
    super.layoutSubviews()
    label.frame = bounds

第二步是定义 MyView 的内在内容大小。在这种情况下,没有固有宽度(因为该尺寸将完全由单元格或其他超视图决定),而固有高度是标签的固有高度:

//  MyView.swift

override var intrinsicContentSize: CGSize 
    return CGSize(width: UIViewNoIntrinsicMetric, height: label.intrinsicContentSize.height)

由于您的标签是多行标签,因此在没有最大宽度的情况下无法确定其固有高度。默认情况下,在没有最大宽度的情况下,UILabel 将返回适合单行标签的固有内容大小,这不是我们想要的。

为计算其内在内容大小而为多行标签提供最大宽度的唯一记录方法是preferredMaxLayoutWidth 属性。 UILabel 的头文件为该属性提供了以下注释:

// 如果非零,则在确定多行标签的-intrinsicContentSize 时使用

所以第三步是确保preferredMaxLayoutWidth被设置为合适的宽度。由于intrinsicContentSize 属性构成自动布局过程的一部分并且仅与自动布局过程相关,并且您的自定义单元子类是执行自动布局的代码的唯一部分,因此设置布局首选项的适当位置在该类中:

//  MyCell.swift

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize 
    myView.label.preferredMaxLayoutWidth = targetSize.width
    myView.invalidateIntrinsicContentSize()

    return super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)

如果 MyView 有多个多行标签,或者如果您希望将 MyView 的子视图层次结构对其父视图隐藏,您同样可以在 MyView 上创建自己的 preferredMaxLayoutWidth 属性并继续执行。

上述代码将确保 MyView 根据其多行标签的内容计算并返回适当的intrinsicContentSize,并且旋转时大小无效。

【讨论】:

UIViewNoIntrinsicMetric 救了我一整天,谢谢【参考方案2】:

这里发生了一些不同的事情,导致您出现这种行为。

您会注意到,如果您在intrinsicContentSize 中打印高度值,它会首先以宽度为零的方式调用,因为视图还没有大小。然后你在 layoutSubviews 中强制标签的框架。此时,单元格已经布局,即使您使用 invalidateIntrinsicContentSize,也不会立即重绘!当您将单元格滚动出视图并返回时,它会在该点重新绘制。

蛮力解决方案是计算 layoutSubviews 内标签的正确高度。或者,在设置稍后应用的文本字符串时计算、保存和存储它。我仍然倾向于首先尝试基于约束的解决方案,它对我来说很有效,即使有多个可变高度标签。有点技巧是基于 superview 的宽度 - 它已经有一个宽度:

override var intrinsicContentSize: CGSize 
    guard
        let text = label.text,
        let parent = superview
    else  return .zero 
    let width = parent.bounds.width
    let height = text.heightWithConstrainedWidth(width: width, font: label.font)
    return CGSize(width: width, height: height)

此外,您正在使用灵活的高度并将 MyView 的所有边缘限制到内容视图。通常,您会将顶部/前导/尾随从 MyView 约束到 Content View,然后将 Content View 的底部约束到 MyView。这样,内容视图的高度将始终弯曲到 MyView 的高度。

【讨论】:

感谢您的回答,但我不能使用 superview hack:在这个用例中它可能会起作用,因为我希望 MyView 的宽度等于单元格的宽度,但在其他地方我的应用我不想要那个。 那么您将不得不使用我建议的其他方法之一。根本问题是在第一次运行时,您没有单元格宽度!您需要其他信息才能在第一条路径上正确设置大小,或强制重绘单元格。【参考方案3】:

您只需要在文本设置器中使固有大小无效,而不是在 layoutSubview() 中执行此操作

func setText(text: String) 
    label.text = text
    invalidateIntrinsicContentSize() // invalidate before layout
    setNeedsLayout()

在 invalidateIntrinsicContentSize() 方法中你也可以简单地做到这一点

override var intrinsicContentSize: CGSize 
    return CGSize(width: bounds.width, height: label.sizeThatFits(CGSize(width: bounds.width, height: CGFloat.greatestFiniteMagnitude)))

【讨论】:

【参考方案4】:

尝试将此添加到viewDidAppear: 的底部

DispatchQueue.main.async  
    self.tableView.reloadData()

表格视图有一个错误,有时会阻止其单元格在第一次加载时正确布局。

【讨论】:

以上是关于如何为具有灵活高度和固定宽度的视图覆盖 intrinsictContentSize?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 WKInterfaceGroup 设置小于 2 点的固定高度或宽度?

如何为运行时计算的变量高度设置布局约束?

如何以编程方式将 UIImageView 缩放到固定宽度和灵活高度?

使 NSView 具有适合内容的固定宽度和可变高度

如何使用 AutoLayout 配置视图宽度固定宽度和可变高度

如何为 UITableViewCell 的 ImageView 设置固定大小?