簡體   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