我一直在努力理解和利用intrinsicSize
在定制上UIView
已经好几天了。到目前为止,收效甚微。
这篇文章很长,对此感到抱歉:-) 问题是,这个主题非常复杂。虽然我知道可能还有其他解决方案,但我只是想了解如何intrinsicSize
可以正确使用。
因此,当有人知道一个关于如何使用/实现的深入解释的好来源时intrinsicSize
你可以跳过我所有的问题,只给我留下链接。
My goal:
创建自定义UIView
它使用intrinsicSize
让 AutoLayout 自动适应不同的内容。就像一个UILabel
它会根据文本内容、字体、字体大小等自动调整大小。
作为一个例子,假设一个简单的视图RectsView
它只是绘制给定数量、给定大小、给定间距的矩形。如果不是所有矩形都适合一行,则内容将被换行并在另一行中继续绘制。因此,视图的高度取决于不同的属性(矩形数量、矩形大小、间距等)
这很像一个UILabel
但绘制的是简单的矩形,而不是单词或字母。然而,虽然UILabel
效果很好,但我无法为我的产品实现同样的效果RectsView
.
为什么选择内在尺寸
正如@DonMag 在他对我之前的问题的出色回答中指出的那样,我不必使用intrinsicSize
实现我的目标。我还可以使用子视图并添加约束来创建这样的矩形图案。或者我可以使用UICollectionView
, etc.
虽然这肯定可行,但我认为这会增加很多开销。如果目标是重新创建一个UILabel
类中,人们不会使用 AutoLayout 或 CollectionView 将字母排列为单词,不是吗?相反,人们肯定会尝试手动绘制字母......尤其是在使用RectsView
在 TableView 或 CollectionView 中,直接绘制的普通视图肯定比使用 AutoLayout 排列的大量子视图编译的复杂解决方案要好。
当然,这是一个极端的例子。然而,归根结底,有些情况下使用intrinsicSize
肯定是更好的选择。自从UILabel
和其他内置视图用途intrinsicSize
完美,必须有一种方法可以让它发挥作用,我只是想知道如何:-)
我对内在尺寸的理解
@DonMag 怀疑,“我真的不明白intrinsicContentSize 做什么和不做什么”。他很可能是正确的:-)但是,问题是我没有找到真正解释它的来源......因此我花了几个小时试图理解如何正确使用intrinsicSize
没有一点进展。
这就是我所学到的从文档 https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/AnatomyofaConstraint.html#//apple_ref/doc/uid/TP40010853-CH9-SW1:
-
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
子类看看如何intrinsicContentSize
用来。
In RectsView
intrinsicContentSize
在边界设置为其最终大小之前仅调用一次。由于此时我还不知道最终尺寸,因此我只能根据旧的、过时的宽度计算固有尺寸,这会导致错误的结果。
In UIView
然而,intrinsicContentSize
被调用多次(why?)并且在最后一次调用中,结果似乎适合即将到来的最终尺寸。此时如何知道这个尺寸?
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
它指定视图在给定维度中没有固有大小。
Why, is intrinsicContentSize
多次调用UILabel
?我尝试返回相同尺寸(65536.0, 20.333333333333332)
in RectsView
但这没有任何区别,intrinsicContentSize
仍然只被调用一次。
在最后一次通话中intrinsicContentSize
of UILabel
价值(338.0, 40.666666666666664)
被返回。看起来UILabel
此时知道,它将被重新调整为宽度350
, but how?
另一个观察结果是,两种观点intrinsicContentSize
不被称为after bounds.didSet
。因此必须有一种方法来知道即将到来的帧变化intrinsicContentSize
before bounds.didSet
. How?
更新2:
我已将调试输出添加到以下内容中,其他UILabel
方法也是如此,但它们是not称为(因此似乎不会影响问题):
sizeToFit()
sizeThatFits(_ :)
contentCompressionResistancePriority(for :)
contentHuggingPriority(for :)
systemLayoutSizeFitting(_ :)
systemLayoutSizeFitting(_ :, withHorizontalFittingPriority :, verticalFittingPriority:)
preferredMaxLayoutWidth get + set