How to override intrinsictContentSize for a view with flexible height and fixed width?
The documentation suggests that overriding intrinsicContentSize
may not be the best way to achieve what you want to achieve. As you've observed, from the UIView class reference:
Setting this property allows a custom view to communicate to the layout system what size it would like to be based on its content. This intrinsic size must be independent of the content frame, because there’s no way to dynamically communicate a changed width to the layout system based on a changed height, for example.
MyView's bounds
are not set until after its intrinsicContentSize
has been requested, and in an auto layout context are not guaranteed to have been set at any time before layoutSubviews()
is called. There is a way to work around that and pass a width to your custom view for use in intrinsicContentSize
, however, which I've included as the second option in this answer.
Option 1: Manual layout by overriding sizeThatFits(_:)
First, in the absence of auto layout, configure MyView and its label subview to size and resize themselves appropriately:
// MyView.swift
func setupView() {
label = UILabel()
label.numberOfLines = 0
label.autoresizingMask = [.flexibleWidth] // New
addSubview(label)
}
override func layoutSubviews() {
super.layoutSubviews()
label.sizeToFit() // New
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return label.sizeThatFits(size) // New
}
MyView will now size itself according to the size of its label subview, and the label will be sized according to the width of its superview (because of its .flexibleWidth
autoresizing mask) and its text content (because numberOfLines
is set to 0). If MyView had other subviews, you would need to compute and return the total size in sizeThatFits(_:)
.
Secondly, we want UITableView to be able to compute the height of its cells and your custom subviews according to the manual layout above. When self-sizing cells, UITableView calls systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority)
on each cell, which in turn calls sizeThatFits(_:)
on the cell. (See WWDC 2014 Session 226.)
The target size or fitting size passed to those methods is the width of the table view with a zero height. The horizontal fitting priority is UILayoutPriorityRequired
. You take control of the self-sizing cell process by overriding one of those methods in your custom cell subclass (the former for auto layout, the latter for manual layout) and using the width of the size passed in to compute and return the height of the cell.
In this case, the size of the cell is the size of myView
:
// MyCell.swift
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return myView.sizeThatFits(targetSize)
}
And you're done.
Option 2: Auto layout by overriding intrinsicContentSize
As the size of MyView depends on the size of its label, the first step is to ensure that the label is positioned and sized correctly. You've already done this:
// MyView.swift
override func layoutSubviews() {
super.layoutSubviews()
label.frame = bounds
}
The second step is to define MyView's intrinsic content size. In this case, there is no intrinsic width (because that dimension will be determined entirely by the cell or other superview), and the intrinsic height is the intrinsic height of the label:
// MyView.swift
override var intrinsicContentSize: CGSize {
return CGSize(width: UIViewNoIntrinsicMetric, height: label.intrinsicContentSize.height)
}
Because your label is a multiline label, its intrinsic height cannot be determined in the absence of a maximum width. By default, in the absence of a maximum width, UILabel will return an intrinsic content size appropriate for a single line label, which is not what we want.
The only documented way to provide a maximum width to a multiline label for the purpose of computing its intrinsic content size is the preferredMaxLayoutWidth
property. The header file for UILabel provides the following comment for that property:
// If nonzero, this is used when determining -intrinsicContentSize for multiline labels
So the third step is to ensure preferredMaxLayoutWidth
is set to an appropriate width. As the intrinsicContentSize
property forms part of, and is relevant only to, the auto layout process, and your custom cell subclass is the only part of your code performing auto layout, the appropriate place to set layout preferences is in that class:
// MyCell.swift
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
myView.label.preferredMaxLayoutWidth = targetSize.width
myView.invalidateIntrinsicContentSize()
return super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
}
If MyView had more than one multiline label, or if you preferred to keep the subview hierarchy of MyView hidden from its superview, you could equally create your own preferredMaxLayoutWidth
property on MyView and follow through.
The above code will ensure MyView computes and return an appropriate intrinsicContentSize
based on the content of its multiline label, and that the size is invalidated on rotation.