几天来,我一直在尝试理解和利用intrinsicSize
一个习惯UIView
。迄今为止收效甚微。
这篇文章很长,很抱歉 :-) 问题是,这个话题很复杂。虽然我知道可能还有其他解决方案,但我只是想了解如何intrinsicSize
正确使用。
因此,当有人知道有关如何使用/实施的深入解释的良好来源时,intrinsicSize
您可以跳过我所有的问题,只留下链接。
我的目标:
创建一个自定义UIView
,用于intrinsicSize
让 AutoLayout 自动采用不同的内容。就像 aUILabel
根据其文本内容、字体、字体大小等自动调整大小一样。
作为一个例子,假设一个简单的视图RectsView
除了绘制给定数量的给定尺寸和给定间距的矩形之外什么都不做。如果不是所有的矩形都适合一行,则内容被包装并在另一行中继续绘制。因此,视图的高度取决于不同的属性(矩形数量、矩形大小、间距等)
这很像一个UILabel
,但不是单词或字母,而是绘制简单的矩形。然而,虽然UILabel
效果很好,但我的RectsView
.
为什么intrinsicSize
正如@DonMag 在他对我上一个问题的出色回答中指出的那样,我不必使用它intrinsicSize
来实现我的目标。我还可以使用子视图并添加约束来创建这样的矩形模式。或者我可以使用 aUICollectionView
等。
虽然这肯定会奏效,但我认为它会增加很多开销。如果目标是重新创建一个UILabel
类,则不会使用 AutoLayout 或 CollectionView 将字母排列成单词,对吗?相反,人们肯定会尝试手动绘制字母......尤其是RectsView
在 TableView 或 CollectionView 中使用直接绘制的普通视图肯定比使用 AutoLayout 排列的大量子视图编译的复杂解决方案要好。
当然,这是一个极端的例子。但是,归根结底,在某些情况下使用intrinsicSize
肯定是更好的选择。由于UILabel
和其他内置视图intrinsicSize
完美使用,因此必须有一种方法可以使其正常工作,我只想知道如何:-)
我对内在尺寸的理解
@DonMag 怀疑,“我并不真正理解 intrinsicContentSize 做什么和不做什么”。他很可能是正确的:-) 但是,问题是我没有找到真正解释它的来源......因此我花了几个小时试图了解如何正确使用intrinsicSize
而没有一点进展。
这是我从文档中学到的:
intrinsicSize
是 中使用的功能AutoLayout
。提供固有高度和/或宽度的视图不需要为这些值指定约束。- 不能保证视图会完全得到它的
intrinsicSize
. 它更像是告诉 autoLayout 哪种尺寸最适合视图,而 autoLayout 将计算实际尺寸。 - 计算是使用
intrinsicSize
和Compression Resistance
+Content Hugging
属性完成的。 - 的计算
intrinsicSize
应该只取决于内容,而不是视图框架。
我不明白的是:
- 计算如何独立于视图框架?当然
UIImageView
可以使用其图像的大小,但UILabel
显然只能根据其内容和宽度来计算 a 的高度。那么我怎么能RectsView
在不考虑框架宽度的情况下计算它的高度呢? - 什么时候应该进行计算
intrinsicSize
?在我的示例中,RectsView
大小取决于矩形大小、间距和数量。在 aUILabel
中,大小还取决于多个属性,如文本、字体、字体大小等。如果在设置每个属性时进行计算,则会执行多次,这是非常低效的。那么什么是正确的地方呢?
示例实现:
这是我的一个简单实现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
s 使用默认宽度而不是实际的最终宽度来计算它们的内容大小。
那么:何时/何处计算初始布局?
此外,对属性的更新(= 点击单元格)未正确处理
那么:何时/何处计算更新的布局?
再次:抱歉,很长的帖子,如果您能读到这里,非常感谢。我非常感谢!
我您知道任何可以解释所有这些的好的来源/教程/方法,我很高兴您可以提供任何链接!
更新:更多观察
我已经向我RectsView
的子类和子类添加了一些调试输出,UILabel
以查看如何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
?我试图返回相同的大小(65536.0, 20.333333333333332)
,RectsView
但这没有任何区别,intrinsicContentSize
仍然只调用一次。
在最后一次调用该值intrinsicContentSize
是返回。似乎知道在这一点上,它将被重新调整为宽度,但是如何呢?UILabel
(338.0, 40.666666666666664)
UILabel
350
另一个观察是,在两个视图上都intrinsicContentSize
没有调用after bounds.didSet
。intrinsicContentSize
因此,必须有一种方法可以知道之前 即将到来的帧变化bounds.didSet
。如何?
更新 2:
我已将调试输出添加到以下其他UILabel
方法中,但未调用它们(因此似乎不会影响问题):
sizeToFit()
sizeThatFits(_ :)
contentCompressionResistancePriority(for :)
contentHuggingPriority(for :)
systemLayoutSizeFitting(_ :)
systemLayoutSizeFitting(_ :, withHorizontalFittingPriority :, verticalFittingPriority:)
preferredMaxLayoutWidth get + set