Thanks @eskimo. I have filed a bug report (FB9012689) and opend a TSI as recommended.
Post
Replies
Boosts
Views
Activity
Example implementation:
Here is a simple implementation of my 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 }
}
}
Problems:
Basicly the intrinsicSize works fine: When the TableView is rendered for the first time each row has a different height depending on its intrinsic content. However, the size is not correctly, since the intrinsicSize is calculated before the TableView actually layouts its subviews. Thus the RectsViews calculate their content size using the default width instead of there acutal, final width.
So: When/Where to calculate the initial layout?
Additionally updates to the properties (= tapping on the cells) are not handled correctly
So: When/Where to calculate the updated layout?
Again: Sorry for the long post and thank you very much if you have managed to read until here. I really appreciate that!
I you know any good source / tutorial / howto which explains all this, I happy about any link you can provide!
UPDATE: Some more observations
I have added some debug output to my RectsView and to a UILabel subclass to see how intrinsicContentSize is used.
In RectsView intrinsicContentSize is called only once before the bounds are set to their final size. Since at this point I not not know the final size yet, I can only calculate the intrinsic size based on the old, outdated width which leads to a wrong result.
In UIView however, intrinsicContentSize is called multiple times (why?) and in the last call, the result seems to be fitting the upcoming, final size. How can this size be known at this point?
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
When called for the first time UILabel returns a intrinsic width of 65536.0 is this some constant? I am only aware of UIView.noIntrinsicMetric = -1 which specifies, that the view does not have a instrinsic size in the given dimension.
Why, is intrinsicContentSize called multiple times on UILabel? I tried to return the same size (65536.0, 20.333333333333332) in RectsView but this does not make any difference, intrinsicContentSize is still only called once.
In the last call to intrinsicContentSize of UILabel the value (338.0, 40.666666666666664) is returend. It seems that UILabel know at this point, that it will be re-sized to a width of 350, but how?
Another oberservation is, that on both views intrinsicContentSize is NOT called after bounds.didSet. Thus there has to be a way to know the upcomming frame changes in intrinsicContentSize before bounds.didSet. How?
UPDATE 2:
I have added debug output to following, other UILabel methods as well, but they are not called (thus seem not to influence the problem):
sizeToFit()
sizeThatFits(_ :)
contentCompressionResistancePriority(for :)
contentHuggingPriority(for :)
systemLayoutSizeFitting(_ :)
systemLayoutSizeFitting(_ :, withHorizontalFittingPriority :, verticalFittingPriority:)
preferredMaxLayoutWidth get + set
I have submitted the feedback including the diagnostics report as advised: FB15429176