繁体   English   中英

如何在自定义 UIView 中正确使用 intrincSize。 在哪里/何时计算和更新大小?

[英]How to correctly use intrincSize in custom UIView. Where/when to calculate and to update the size?

几天来,我一直在尝试在自定义UIView上理解和利用intrinsicSize 迄今为止收效甚微。

这篇文章很长,很抱歉:-) 问题是,这个话题很复杂。 虽然我知道可能有其他解决方案,但我只是想了解如何正确使用intrinsicSize

因此,当有人知道关于如何使用/实现intrinsicSize的深入解释的良好来源时,您可以跳过我的所有问题并留下链接。

我的目标:

创建一个自定义UIView ,它使用intrinsicSize让 AutoLayout 自动采用不同的内容。 就像UILabel根据其文本内容、字体、字体大小等自动调整大小一样。

举个例子,假设一个简单的视图RectsView ,它除了绘制给定数量的给定尺寸和给定间距的矩形之外什么都不做。 如果不是所有的矩形都适合一行,则内容被包装并在另一行中继续绘制。 因此,视图的高度取决于不同的属性(矩形数量、矩形大小、间距等)

这与UILabel非常相似,但绘制的不是单词或字母,而是简单的矩形。 然而,虽然UILabel工作得很好,但我无法为我的RectsView实现同样的效果。

为什么intrinsicSize

正如@DonMag 在他对我之前的问题的出色回答中指出的那样,我不必使用intrinsicSize来实现我的目标。 我还可以使用子视图并添加约束来创建这样的矩形模式。 或者我可以使用UICollectionView等。

虽然这肯定会奏效,但我认为它会增加很多开销。 如果目标是重新创建UILabel class,则不会使用 AutoLayout 或 CollectionView 将字母排列成单词,对吗? 相反,人们肯定会尝试手动绘制字母......尤其是在 TableView 或 CollectionView 中使用RectsView时,直接绘制的普通视图肯定比使用 AutoLayout 排列的大量子视图编译的复杂解决方案要好。

当然,这是一个极端的例子。 然而,归根结底,在某些情况下使用intrinsicSize肯定是更好的选择。 由于UILabel和其他内置视图完美地使用了intrinsicSize ,因此必须有一种方法可以使其正常工作,我只想知道如何:-)

我对内在尺寸的理解

@DonMag 怀疑, “我并不真正了解 intrinsicContentSize 做什么和不做什么” 他很可能是正确的:-) 然而,问题是我没有找到真正解释它的来源......因此我花了几个小时试图理解如何正确使用intrinsicSize而没有一点进展。

这是我从文档中学到的:

  • intrinsicSizeAutoLayout中使用的一项功能。 提供固有高度和/或宽度的视图不需要为这些值指定约束。
  • 不能保证视图会准确地得到它的intrinsicSize 它更像是告诉 autoLayout 哪种尺寸最适合视图,而 autoLayout 将计算实际尺寸。
  • 计算是使用intrinsicSizeCompression Resistance + Content Hugging属性完成的。
  • intrinsicSize的计算应该只取决于内容,而不是视图框架。

我不明白的是:

  • 计算如何独立于视图框架? 当然UIImageView可以使用其图像的大小,但UILabel的高度显然只能根据其内容和宽度来计算。 那么我的RectsView如何在不考虑框架宽度的情况下计算它的高度呢?
  • 什么时候应该计算intrinsicSize 在我的RectsView示例中,大小取决于矩形大小、间距和数量。 UILabel中,大小还取决于多个属性,如文本、字体、字体大小等。如果在设置每个属性时进行计算,则会执行多次,这是非常低效的。 那么什么是正确的地方呢?

示例实现:

这是我的RectsView的简单实现:

@IBDesignable class RectsView: UIView {
    
    // Properties which determin the intrinsic height
    @IBInspectable public var rectSize: CGFloat = 20 {
        didSet {
            calcContent()
            setNeedsLayout()
        }
    }
    
    @IBInspectable public var rectSpacing: CGFloat = 10 {
        didSet {
            calcContent()
            setNeedsLayout()
        }
    }
    
    @IBInspectable public var rowSpacing: CGFloat = 5 {
        didSet {
            calcContent()
            setNeedsLayout()
        }
    }
    
    @IBInspectable public var rectsCount: Int = 20 {
        didSet {
            calcContent()
            setNeedsLayout()
        }
    }
    
    
    // Calculte the content and its height
    private var rects = [CGRect]()
    func calcContent() {
        var x: CGFloat = 0
        var y: CGFloat = 0
        
        rects = []
        if rectsCount > 0 {
            for _ in 0..<rectsCount {
                let rect = CGRect(x: x, y: y, width: rectSize, height: rectSize)
                rects.append(rect)
                
                x += rectSize + rectSpacing
                if x + rectSize > frame.width {
                    x = 0
                    y += rectSize + rowSpacing
                }
            }
        }
        
        height = y + rectSize
        invalidateIntrinsicContentSize()
    }
    
    
    // Intrinc height
    @IBInspectable var height: CGFloat = 50
    override var intrinsicContentSize: CGSize {
        return CGSize(width: UIView.noIntrinsicMetric, height: height)
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        invalidateIntrinsicContentSize()
    }
    
    
    // Drawing
    override func draw(_ rect: CGRect) {
        super.draw(rect)
                
        let context = UIGraphicsGetCurrentContext()
        
        for rect in rects {
            context?.setFillColor(UIColor.red.cgColor)
            context?.fill(rect)
        }
        
        
        let attrs = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: UIFont.systemFontSize)]
        let text = "\(height)"
        
        text.draw(at: CGPoint(x: 0, y: 0), withAttributes: attrs)
    }
}

class ViewController: UITableViewController {
    let CellId = "CellId"

    // Dummy content with different values per row
    var data: [(CGFloat, CGFloat, CGFloat, Int)] = [
        (10.0, 15.0, 13.0, 35),
        (20.0, 10.0, 16.0, 28),
        (30.0, 5.0, 19.0, 21)
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "IntrinsicCell", bundle: nil), forCellReuseIdentifier: CellId)
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CellId, for: indexPath) as? IntrinsicCell ?? IntrinsicCell()
        
        // Dummy content with different values per row
        cell.rectSize = data[indexPath.row].0     // CGFloat((indexPath.row+1) * 10)
        cell.rectSpacing = data[indexPath.row].1  // CGFloat(20 - (indexPath.row+1) * 5)
        cell.rowSpacing = data[indexPath.row].2   // CGFloat(10 + (indexPath.row+1) * 3)
        cell.rectsCount = data[indexPath.row].3   // (5 - indexPath.row) * 7
        
        return cell
    }

    // Add/remove content when tapping on a row
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        var rowData = data[indexPath.row]
        rowData.3 = (indexPath.row % 2 == 0 ? rowData.3 + 5 : rowData.3 - 5)
        data[indexPath.row] = rowData
        tableView.reloadRows(at: [indexPath], with: .automatic)
    }
}


// Simple cell holing an intrinsic rectsView
class IntrinsicCell: UITableViewCell {
    @IBOutlet private var rectsView: RectsView!
        
    var rectSize: CGFloat {
        get { return rectsView.rectSize }
        set { rectsView.rectSize = newValue }
    }
    
    var rectSpacing: CGFloat {
        get { return rectsView.rectSpacing }
        set { rectsView.rectSpacing = newValue }
    }
    
    var rowSpacing: CGFloat {
        get { return rectsView.rowSpacing }
        set { rectsView.rowSpacing = newValue }
    }
    
    var rectsCount: Int {
        get { return rectsView.rectsCount }
        set { rectsView.rectsCount = newValue }
    }
} 

问题:

基本上, intrinsicSize工作正常:第一次呈现 TableView 时,每行根据其内在内容具有不同的高度。 但是,大小不正确,因为intrinsicSize是在TableView 实际布局其子视图之前计算的。 因此RectsView使用默认宽度而不是实际的最终宽度来计算它们的内容大小。

那么:何时/何处计算初始布局?

此外,对属性的更新(= 点击单元格)未正确处理

那么:何时/何处计算更新的布局?

在此处输入图像描述

再次:抱歉,很长的帖子,如果您能读到这里,非常感谢。 我非常感谢!

我您知道任何可以解释所有这些的好的来源/教程/方法,我很高兴您可以提供任何链接!

更新:更多观察

我在我的RectsViewUILabel子类中添加了一些调试 output 以查看如何使用intrinsicContentSize

RectsView intrinsicContentSize仅在边界设置为其最终大小之前调用一次。 由于此时我还不知道最终尺寸,我只能根据旧的、过时的宽度来计算内在尺寸,这会导致错误的结果。

然而,在UIView中, intrinsicContentSize被多次调用(为什么? ),在最后一次调用中,结果似乎适合即将到来的最终大小。 在这一点上如何知道这个大小?

RectsView willSet frame: (-120.0, -11.5, 240.0, 23.0)
RectsView didSet frame: (40.0, 11.0, 240.0, 23.0)
RectsView didSet rectSize: 10.0
RectsView didSet rectSpacing: 15.0
RectsView didSet rowSpacing: 20
RectsView didSet rectsCount: 35
RectsView get intrinsicContentSize: 79.0
RectsView willSet bounds: (0.0, 0.0, 240.0, 23.0)
RectsView didSet bounds: (0.0, 0.0, 350.0, 79.33333333333333)
RectsView layoutSubviews
RectsView layoutSubviews

MyLabel willSet frame: (-116.5, -9.5, 233.0, 19.0)
MyLabel didSet frame: (53.0, 13.0, 233.0, 19.0)
MyLabel willSet text: (53.0, 13.0, 233.0, 19.0)
MyLabel didSet text: (53.0, 13.0, 233.0, 19.0)
MyLabel get intrinsicContentSize: (65536.0, 20.333333333333332)
MyLabel get intrinsicContentSize: (675.0, 20.333333333333332)
MyLabel get intrinsicContentSize: (65536.0, 20.333333333333332)
MyLabel get intrinsicContentSize: (338.0, 40.666666666666664)
MyLabel get intrinsicContentSize: (65536.0, 20.333333333333332)
MyLabel get intrinsicContentSize: (65536.0, 20.333333333333332)
MyLabel get intrinsicContentSize: (338.0, 40.666666666666664)
MyLabel get intrinsicContentSize: (65536.0, 20.333333333333332)
MyLabel get intrinsicContentSize: (338.0, 40.666666666666664)
MyLabel willSet bounds: (0.0, 0.0, 233.0, 19.0)
MyLabel didSet bounds: (0.0, 0.0, 350.0, 41.0)
MyLabel layoutSubviews
MyLabel layoutSubviews

当第一次调用UILabel返回一个65536.0的固有宽度时,这是一些常数吗? 我只知道UIView.noIntrinsicMetric = -1指定视图在给定维度中没有内在大小。

为什么, intrinsicContentSizeUILabel上被多次调用? 我试图在RectsView中返回相同的大小(65536.0, 20.333333333333332) ,但这没有任何区别, intrinsicContentSize仍然只调用一次。

在最后一次调用UILabelintrinsicContentSize时,返回值(338.0, 40.666666666666664) 似乎UILabel在这一点上知道,它将被重新调整为350的宽度,但是如何?

另一个观察是,在这两个视图上, intrinsicContentSizebounds.didSet之后都没有被调用。 因此,必须有一种方法可以在bounds.didSet之前知道intrinsicContentSize中即将到来的帧变化。 如何?

更新 2:

我已将调试 output 添加到以下其他UILabel方法中,但未调用它们(因此似乎不会影响问题):

sizeToFit()
sizeThatFits(_ :)
contentCompressionResistancePriority(for :)
contentHuggingPriority(for :)
systemLayoutSizeFitting(_ :)
systemLayoutSizeFitting(_ :, withHorizontalFittingPriority :, verticalFittingPriority:)
preferredMaxLayoutWidth  get + set

正如@DonMag 建议的那样,我使用技术支持事件直接从 Apple 工程师那里获得反馈。 花了一些时间才得到答案,但最终还是到了:

长话短说:不要使用内在大小

似乎内在大小更像是一个不打算由自定义视图使用的内部功能。 当然它不是私有的 API 并且可以使用。 但是,如果没有(很多)与其他私有 iOS 功能一起使用,它非常有限,几乎无法使用。

Apple 建议改用 AutoLayout 和约束。 即使对于像自定义 UILabel 克隆这样的基本示例也是如此。

遇到了这个问题: UITableViewCell with intrinsic height based on width这似乎是一个类似的问题。

该解决方案是覆盖单元格 class 中的systemLayoutSizeFitting(...) 似乎适用于您的情况 - 在RectsView中进行必要的更改以在bounds更改时重新计算。

看看这对你有什么影响:

@IBDesignable class RectsView: UIView {
    
    // calcContent needs to be called
    //  when any of these values are changed
    
    // Properties which determin the intrinsic height
    @IBInspectable public var rectSize: CGFloat = 20 {
        didSet {
            calcContent(bounds.width)
        }
    }
    
    @IBInspectable public var rectSpacing: CGFloat = 10 {
        didSet {
            calcContent(bounds.width)
        }
    }
    
    @IBInspectable public var rowSpacing: CGFloat = 5 {
        didSet {
            calcContent(bounds.width)
        }
    }
    
    @IBInspectable public var rectsCount: Int = 20 {
        didSet {
            calcContent(bounds.width)
        }
    }
    
    // Calculte the content and its height
    private var rects = [CGRect]()
    
    // using newWidth parameter allows us to
    //  pass the new bounds width when
    //  the bounds is set
    func calcContent(_ newWidth: CGFloat) {
        var x: CGFloat = 0
        var y: CGFloat = 0
        
        rects = []
        if rectsCount > 0 {
            for i in 0..<rectsCount {
                let rect = CGRect(x: x, y: y, width: rectSize, height: rectSize)
                rects.append(rect)
                
                x += rectSize + rectSpacing
                if x + rectSize > newWidth && i < rectsCount - 1 {
                    x = 0
                    y += rectSize + rowSpacing
                }
            }
        }
        
        height = y + rectSize
        
        // need to trigger draw()
        setNeedsDisplay()
    }
    
    // Intrinsic height
    @IBInspectable var height: CGFloat = 10 {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }
    override var intrinsicContentSize: CGSize {
        return CGSize(width: UIView.noIntrinsicMetric, height: height)
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        invalidateIntrinsicContentSize()
    }
    
    // Drawing
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        let context = UIGraphicsGetCurrentContext()
        
        for rect in rects {
            context?.setFillColor(UIColor.red.cgColor)
            context?.fill(rect)
        }
        
        let attrs = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: UIFont.systemFontSize)]
        let text = "\(height)"
        
        text.draw(at: CGPoint(x: 0, y: 0), withAttributes: attrs)
    }
    
    override var bounds: CGRect {
        willSet {
            calcContent(newValue.width)
        }
    }
}

// Simple cell holing an intrinsic rectsView
class IntrinsicCell: UITableViewCell {
    @IBOutlet var rectsView: RectsView!
    
    var rectSize: CGFloat {
        get { return rectsView.rectSize }
        set { rectsView.rectSize = newValue }
    }
    
    var rectSpacing: CGFloat {
        get { return rectsView.rectSpacing }
        set { rectsView.rectSpacing = newValue }
    }
    
    var rowSpacing: CGFloat {
        get { return rectsView.rowSpacing }
        set { rectsView.rowSpacing = newValue }
    }
    
    var rectsCount: Int {
        get { return rectsView.rectsCount }
        set { rectsView.rectsCount = newValue }
    }
    
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        //force layout of all subviews including RectsView, which
        //updates RectsView's intrinsic height, and thus height of a cell
        self.setNeedsLayout()
        self.layoutIfNeeded()
        
        //now intrinsic height is correct, so we can call super method
        return super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
    }
    
}

class ViewController: UITableViewController {
    let CellId = "CellId"
    
    // Dummy content with different values per row
    var data: [(CGFloat, CGFloat, CGFloat, Int)] = [
        (10.0, 15.0, 13.0, 35),
        (20.0, 10.0, 16.0, 28),
        (30.0, 5.0, 19.0, 21),
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //tableView.register(UINib(nibName: "IntrinsicCell", bundle: nil), forCellReuseIdentifier: CellId)
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCell(withIdentifier: CellId, for: indexPath) as! IntrinsicCell
        
        // Dummy content with different values per row
        cell.rectSize = data[indexPath.row].0     // CGFloat((indexPath.row+1) * 10)
        cell.rectSpacing = data[indexPath.row].1  // CGFloat(20 - (indexPath.row+1) * 5)
        cell.rowSpacing = data[indexPath.row].2   // CGFloat(10 + (indexPath.row+1) * 3)
        cell.rectsCount = data[indexPath.row].3   // (5 - indexPath.row) * 7
        
        return cell
        
    }
    
    // Add/remove content when tapping on a row
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        var rowData = data[indexPath.row]
        rowData.3 = (indexPath.row % 2 == 0 ? rowData.3 + 5 : rowData.3 - 5)
        data[indexPath.row] = rowData
        tableView.reloadRows(at: [indexPath], with: .automatic)
    }
} 

编辑

是的,这将是一种解决方法......尽管,如果它解决了问题,也许不是一件坏事。

正如您所指出的, UILabel可以根据最终宽度正确地重新计算其intrinsicContentSize ——这似乎无法及时用于编写此代码的方式。

查看UILabel.h (搜索 iOS 运行时标头),您会看到很多我们不知道的“幕后”内容。

暂无
暂无

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

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