[英]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
而沒有一點進展。
這是我從文檔中學到的:
intrinsicSize
是AutoLayout
中使用的一項功能。 提供固有高度和/或寬度的視圖不需要為這些值指定約束。intrinsicSize
。 它更像是告訴 autoLayout 哪種尺寸最適合視圖,而 autoLayout 將計算實際尺寸。intrinsicSize
和Compression 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
使用默認寬度而不是實際的最終寬度來計算它們的內容大小。
那么:何時/何處計算初始布局?
此外,對屬性的更新(= 點擊單元格)未正確處理
那么:何時/何處計算更新的布局?
再次:抱歉,很長的帖子,如果您能讀到這里,非常感謝。 我非常感謝!
我您知道任何可以解釋所有這些的好的來源/教程/方法,我很高興您可以提供任何鏈接!
更新:更多觀察
我在我的RectsView
和UILabel
子類中添加了一些調試 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
指定視圖在給定維度中沒有內在大小。
為什么, intrinsicContentSize
在UILabel
上被多次調用? 我試圖在RectsView
中返回相同的大小(65536.0, 20.333333333333332)
,但這沒有任何區別, intrinsicContentSize
仍然只調用一次。
在最后一次調用UILabel
的intrinsicContentSize
時,返回值(338.0, 40.666666666666664)
。 似乎UILabel
在這一點上知道,它將被重新調整為350
的寬度,但是如何?
另一個觀察是,在這兩個視圖上, intrinsicContentSize
在bounds.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.