iOS - 自定义视图:在 layoutSubviews() 中更新后忽略内部内容大小
iOS - Custom view: Intrinsic content size ignored after updated in layoutSubviews()
我有一个表格视图单元格,其中包含自定义视图以及其他视图,并且使用了自动布局。
我的自定义视图的目的是按行布置其子视图,如果当前子视图不适合当前行,则分成新行。它有点像多行标签,但有视图。我通过精确定位而不是自动布局实现了这一点。
由于我只知道layoutSubviews()
中的视图宽度,因此我需要计算那里的确切位置和行数。效果很好,但我的视图的框架(零)不匹配,因为缺少 intrinsicContentSize
.
因此,如果我的身高自上次布局通过后发生变化,我会在计算的末尾添加一个检查项。如果是的话,我会更新 intrinsicContentSize
属性 中使用的高度 属性 并调用 invalidateIntrinsicContentSize()
.
我观察到最初 layoutSubviews()
被调用了两次。第一遍效果很好,即使单元格的宽度小于应有的宽度,也会考虑 intrinsicContentSize
。第二遍使用实际宽度并更新 intrinsicContentSize
。但是父级(tableview 单元格中的 contentView)忽略了这个新的 intrinsicContentSize
.
所以基本上结果是子视图的布局和绘制正确,但自定义视图的框架不是 updated/used 在父视图中。
问题:
有没有办法通知父级有关固有大小的更改或指定位置更新在 layoutSubviews()
中计算的大小以便在父级中使用新大小?
编辑:
这是我的自定义视图中的代码。
仅供参考:8 只是两个子视图之间的垂直和水平 space
class WrapView : UIView {
var height = CGFloat.zero
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: height)
}
override func layoutSubviews() {
super.layoutSubviews()
guard frame.size.width != .zero else { return }
// Make subviews calc their size
subviews.forEach { [=11=].sizeToFit() }
// Check if there is enough space in a row to fit at least one view
guard subviews.map({ [=11=].frame.size.width }).max() ?? .zero <= frame.size.width else { return }
let width = frame.size.width
var row = [UIView]()
// rem is the remaining space in the current row
var rem = width
var y: CGFloat = .zero
var i = 0
while i < subviews.count {
let view = subviews[i]
let sizeNeeded = view.frame.size.width + (row.isEmpty ? 0 : 8)
let last = i == subviews.count - 1
let fit = rem >= sizeNeeded
if fit {
row.append(view)
rem -= sizeNeeded
i += 1
guard last else { continue }
}
let rowWidth = row.map { [=11=].frame.size.width + 8 }.reduce(-8, +)
var x = (width - rowWidth) * 0.5
for vw in row {
vw.frame.origin = CGPoint(x: x, y: y)
x += vw.frame.width + 8
}
y += row.map { [=11=].frame.size.height }.max()! + 8
rem = width
row = []
}
if height != y - 8 {
height = y - 8
invalidateIntrinsicContentSize()
}
}
}
经过大量的尝试和研究,我终于解决了这个错误。
正如@DonMag 在评论中提到的那样,新的单元格大小直到新的布局通过才被识别。这可以通过滚动显示正确布局的单元格 off-screen 并返回来验证。不幸的是,由于 .beginUpdates()
+ .endUpdates()
没有触发新通行证,因此比预期的要难
完成任务。
无论如何,我没有找到触发它的方法,但我按照 answer 中描述的说明进行了操作。特别是带有用于高度计算的原型单元格的部分提供了一个可以在 tableview(heightForRowAt:)
.
中返回的值
Swift 5:
这是用于计算的代码:
let fitSize = CGSize(width: view.frame.size.width, height: .zero)
/* At this point populate the cell with the exact same data as the actual cell in the tableview */
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.bounds = CGRect(x: .zero, y: .zero, width: view.frame.size.width, height: cell.bounds.height)
cell.setNeedsLayout()
cell.layoutIfNeeded()
height = headerCell.contentView.systemLayoutSizeFitting(fitSize).height + 1
该值只计算一次并缓存,因为在我的情况下大小不再改变。
然后在委托中可以返回值:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? height : UITableView.automaticDimension
}
我只用于第一个单元格,因为它是我的 header 单元格并且只有一个部分。
我有一个表格视图单元格,其中包含自定义视图以及其他视图,并且使用了自动布局。
我的自定义视图的目的是按行布置其子视图,如果当前子视图不适合当前行,则分成新行。它有点像多行标签,但有视图。我通过精确定位而不是自动布局实现了这一点。
由于我只知道layoutSubviews()
中的视图宽度,因此我需要计算那里的确切位置和行数。效果很好,但我的视图的框架(零)不匹配,因为缺少 intrinsicContentSize
.
因此,如果我的身高自上次布局通过后发生变化,我会在计算的末尾添加一个检查项。如果是的话,我会更新 intrinsicContentSize
属性 中使用的高度 属性 并调用 invalidateIntrinsicContentSize()
.
我观察到最初 layoutSubviews()
被调用了两次。第一遍效果很好,即使单元格的宽度小于应有的宽度,也会考虑 intrinsicContentSize
。第二遍使用实际宽度并更新 intrinsicContentSize
。但是父级(tableview 单元格中的 contentView)忽略了这个新的 intrinsicContentSize
.
所以基本上结果是子视图的布局和绘制正确,但自定义视图的框架不是 updated/used 在父视图中。
问题:
有没有办法通知父级有关固有大小的更改或指定位置更新在 layoutSubviews()
中计算的大小以便在父级中使用新大小?
编辑:
这是我的自定义视图中的代码。
仅供参考:8 只是两个子视图之间的垂直和水平 space
class WrapView : UIView {
var height = CGFloat.zero
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: height)
}
override func layoutSubviews() {
super.layoutSubviews()
guard frame.size.width != .zero else { return }
// Make subviews calc their size
subviews.forEach { [=11=].sizeToFit() }
// Check if there is enough space in a row to fit at least one view
guard subviews.map({ [=11=].frame.size.width }).max() ?? .zero <= frame.size.width else { return }
let width = frame.size.width
var row = [UIView]()
// rem is the remaining space in the current row
var rem = width
var y: CGFloat = .zero
var i = 0
while i < subviews.count {
let view = subviews[i]
let sizeNeeded = view.frame.size.width + (row.isEmpty ? 0 : 8)
let last = i == subviews.count - 1
let fit = rem >= sizeNeeded
if fit {
row.append(view)
rem -= sizeNeeded
i += 1
guard last else { continue }
}
let rowWidth = row.map { [=11=].frame.size.width + 8 }.reduce(-8, +)
var x = (width - rowWidth) * 0.5
for vw in row {
vw.frame.origin = CGPoint(x: x, y: y)
x += vw.frame.width + 8
}
y += row.map { [=11=].frame.size.height }.max()! + 8
rem = width
row = []
}
if height != y - 8 {
height = y - 8
invalidateIntrinsicContentSize()
}
}
}
经过大量的尝试和研究,我终于解决了这个错误。
正如@DonMag 在评论中提到的那样,新的单元格大小直到新的布局通过才被识别。这可以通过滚动显示正确布局的单元格 off-screen 并返回来验证。不幸的是,由于 .beginUpdates()
+ .endUpdates()
没有触发新通行证,因此比预期的要难
完成任务。
无论如何,我没有找到触发它的方法,但我按照 answer 中描述的说明进行了操作。特别是带有用于高度计算的原型单元格的部分提供了一个可以在 tableview(heightForRowAt:)
.
Swift 5:
这是用于计算的代码:
let fitSize = CGSize(width: view.frame.size.width, height: .zero)
/* At this point populate the cell with the exact same data as the actual cell in the tableview */
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.bounds = CGRect(x: .zero, y: .zero, width: view.frame.size.width, height: cell.bounds.height)
cell.setNeedsLayout()
cell.layoutIfNeeded()
height = headerCell.contentView.systemLayoutSizeFitting(fitSize).height + 1
该值只计算一次并缓存,因为在我的情况下大小不再改变。
然后在委托中可以返回值:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? height : UITableView.automaticDimension
}
我只用于第一个单元格,因为它是我的 header 单元格并且只有一个部分。