I have a tableview cell containing a custom view among other views and autolayout is used.
The purpose of my custom view is to layout its subviews in rows and breaks into a new row if the current subview does not fit in the current line. It kind of works like a multiline label but with views. I achieved this through exact positioning instead of autolayout.
Since I only know the width of my view in layoutSubviews()
, I need to calculate the exact positions and number of lines there. This worked out well, but the frame(zero) of my view didn't match because of missing intrinsicContentSize
.
So I added a check to the end of my calculation if my height changed since the last layout pass. If it did I update the height property which is used in my intrinsicContentSize
property and call invalidateIntrinsicContentSize()
.
I observed that initially layoutSubviews()
is called twice. The first pass works well and the intrinsicContentSize
is taken into account even though the width of the cell is smaller than it should be. The second pass uses the actual width and also updates the intrinsicContentSize
. However the parent(contentView in tableview cell) ignores this new intrinsicContentSize
.
So basically the result is that the subviews are layout and drawn correctly but the frame of the custom view is not updated/used in parent.
The question:
Is there a way to notify the parent about the change of the intrinsic size or a designated place to update the size calculated in layoutSubviews()
so the new size is used in the parent?
Edit:
Here is the code in my custom view.
FYI: 8 is just the vertical and horizontal space between two subviews
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 { $0.sizeToFit() }
// Check if there is enough space in a row to fit at least one view
guard subviews.map({ $0.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 { $0.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 { $0.frame.size.height }.max()! + 8
rem = width
row = []
}
if height != y - 8 {
height = y - 8
invalidateIntrinsicContentSize()
}
}
}