iOS intrinsicContentSize的探索

1、intrinsicContentSize是什麼?先看蘋果官方的介紹:

Apple-intrinsicContentSizeswift

The natural size for the receiving view, considering only properties of the view itself.markdown

Return Value:

A size indicating the natural size for the receiving view based on its intrinsic properties.app

Discussion:

The default width and height values of this property are set to NSViewNoInstrinsicMetric. For a custom view, you can override this property and use it to communicate what size you would like your view to be based on its content. You might do this in cases where the layout system cannot determine the size of the view based solely on its current constraints. For example, a text field might override this method and return an intrinsic size based on the text it contains. The intrinsic size you supply must be independent of the content frame, because there’s no way to dynamically communicate a changed width to the layout system based on a changed height. If your custom view has no intrinsic size for a given dimension, you can set the corresponding dimension to the NSViewNoInstrinsicMetric.ide

也就是說,若是系統佈局沒法根據當前約束來肯定視圖大小,自定義view能夠重寫屬性intrinsicContentSize來決定本身的大小。oop

2、寫個demo測試一下:

建個空的viewController,在viewDidLoad()打印view的intrinsicContentSize:
override func viewDidLoad() {
    super.viewDidLoad()
    print("view intrinsic content size: \(view.intrinsicContentSize)")
}
複製代碼

控制檯輸出:佈局

view intrinsic content size: (-1.0, -1.0)
複製代碼

說明對於一個view來講,默認的intrinsicContentSize值是(-1.0, -1.0)測試

換個label試試,設置佈局居中:
private var label: UILabel = {
    let label = UILabel()
    label.backgroundColor = .red
    label.text = "Label"
    label.textColor = .black
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
}()
    
override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(label)
    NSLayoutConstraint.activate([
        label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
    ])
    print("label intrinsic content size: \(label.intrinsicContentSize)")
}
複製代碼

控制檯輸出:ui

label intrinsic content size: (41.333333333333336, 20.333333333333332)
複製代碼

image.png

說明對於有text內容的控件,UILabel,intrinsicContentSize的值會自動被設置。this

那給UIButton設個text,是否是也能像label那樣呢?
private var button: UIButton = {
    let button = UIButton()
    button.backgroundColor = .yellow
    button.setTitle("Button", for: .normal)
    button.setTitleColor(.black, for: .normal)
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
}()

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    view.addSubview(button)
    NSLayoutConstraint.activate([
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        button.topAnchor.constraint(equalTo: label.bottomAnchor)
    ])
    print("button intrinsic content size: \(button.intrinsicContentSize)")
}
複製代碼

控制檯輸出:spa

button intrinsic content size: (54.0, 34.0)
複製代碼

image.png

說明UIButton也是和UILabel同樣,intrinsicContentSize的值會被自動設置。

如今試試自定義的UIView

直接新建個DemoView類

final class DemoView: UIView {}
複製代碼

把它也加到controller的view上去

private var demoView: DemoView = {
    let view = DemoView()
    view.backgroundColor = .blue
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    view.addSubview(demoView)
    NSLayoutConstraint.activate([
        demoView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        demoView.topAnchor.constraint(equalTo: button.bottomAnchor)
    ])
    print("demoView intrinsic content size: \(demoView.intrinsicContentSize)")
}
複製代碼

控制檯輸出:

demoView intrinsic content size: (-1.0, -1.0)
複製代碼

並且模擬器上並未出現demoView。

image.png

看來和輸出controller的view的結果同樣。

在DemoView裏重寫intrinsicContentSize試試
final class DemoView: UIView {
    override var intrinsicContentSize: CGSize {
        return CGSize(width: 100, height: 50)
    }
}
複製代碼

再運行,能夠發現輸出:

demoView intrinsic content size: (100.0, 50.0)
複製代碼

並且界面上出現了demoView

image.png

這就說明了若是系統佈局沒法根據當前約束來肯定視圖大小,自定義view能夠重寫屬性intrinsicContentSize來決定本身的大小。

再試試若是改變width的值爲UIView.noIntrinsicMetric:
final class DemoView: UIView {
    override var intrinsicContentSize: CGSize {
        return CGSize(width: UIView.noIntrinsicMetric, height: 50)
    }
}
複製代碼

運行,輸出:

demoView intrinsic content size: (-1.0, 50.0)
複製代碼

界面上沒出現demoView:

image.png

我的猜想:系統認爲你的width設置成UIView.noIntrinsicMetric,就是表明你在約束上給了可肯定的結果。實際上並無可肯定的寬度的約束,因此就顯示失敗了。是否是這樣呢?

來改變一下demoView的寬度約束和label的一致:
NSLayoutConstraint.activate([
    ...
    demoView.widthAnchor.constraint(equalTo: label.widthAnchor)
])
複製代碼

運行,輸出:

demoView intrinsic content size: (-1.0, 50.0)
複製代碼

界面上出現了demoView,並且寬度和label的一致:

image.png

這就說明若是寬度約束能讓系統肯定寬度的狀況下,能夠給intrinsicContentSize的width設置爲UIView.noIntrinsicMetric,同理,height也同樣。

3、實際項目中的應用狀況:

項目中須要實現dynamic scaling動態縮放功能,相應控件要隨着系統的縮放級別而變化。

demo.gif

先來看看拆分視圖上的控件狀況:

image.png

先理清楚一些細節:

  1. ①UITableView並無設置自動估算行高,也沒有使用heightForRow方法返回固定的行高。

  2. ②UITableViewCell裏面的init方法裏給自定義View③作了自動佈局,距離上下左右,但沒有設置高度。

  3. ③自定義View裏面的④UICollectinView佈局也是距離上下左右,沒有設置高度。

  4. ③自定義View裏面重寫了intrinsicContentSize屬性,根據縮放比例算出cell的高度,寬度使用的是UIView.noIntrinsicMetric。

  5. 設置縮放級別,會觸發tableView的cellForRow方法,setupCell,③的size會根據intrinsicContentSize而獲得,因此界面會實時縮放。

  6. ④UICollectinView的flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize,並沒什麼用。

在這個基礎上,把重寫了intrinsicContentSize屬性的邏輯註釋掉,會發生什麼?

image.png

啥都沒了。只剩下個空空如也的Cell。

這時候去設置tableView的自動估算高度:

tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 100
複製代碼

再運行:

image.png

仍是空空如也,說明自定義UIView塞在cell裏,tableView是沒辦法自動估計到UIView的高度的。因此最簡單的方式就是設置③自定義View裏的intrinsicContentSize

若是不使用intrinsicContentSize,那就須要在③自定義View裏開放個屬性來計算數據實際高度,而後在tableView的heightForCell裏寫死這個高度了。

4、監聽系統的動態縮放級別

  • 設置監聽(建議在實現intrinsicContentSize的UIView裏進行監聽,這樣就有完整的封裝性,而不是在UIView外部):
    NotificationCenter.default.addObserver(self, selector: #selector(didChangeContentSizeCategory), name: UIContentSizeCategory.didChangeNotification, object: nil)
    複製代碼
  • 在監聽方法中,調用invalidateIntrinsicContentSize(),系統會從新給UIView設置intrinsicContentSize,這樣界面就會實現實時縮放:
    @objc private func didChangeContentSizeCategory() {
        invalidateIntrinsicContentSize()
        ...
    }
    複製代碼

5、參考文獻

developer.apple.com/documentati…

medium.com/@vialyx/imp…

www.hackingwithswift.com/example-cod…

相關文章
相關標籤/搜索