iosdev

Multi-line UILabel in self-sizing CollectionView layouts

One small trick to make creation of self-sizing layouts easier.

Building self-sizing reusable views in custom UICollectionViewLayout instances is more or less straight-forward. Let’s say you want to replicate table view using custom (Flow)Layout.

First, content of your reusable views – whether cells or supplementary views – should be properly setup with auto layout.

Second, in your reusable view subclass you implement this method:

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
	let attr = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
	layoutIfNeeded()

	let fittedSize = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize,
		withHorizontalFittingPriority: UILayoutPriority.required,
		verticalFittingPriority: UILayoutPriority.fittingSizeLevel)
	attr.frame.size.height = ceil(fittedSize.height)
	return attr
}

This enforces layout pass then asks for minimal fitting CGSize using very low priority for vertical axis and highest possible priority for horizontal axis. This allows content to take as much height it needs while not exceeding assigned width.

Third, your UICollectionViewLayout subclass needs to listen to the output from the previous method, for each reusable view:

override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes,
	withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool
{
	if preferredAttributes.frame == originalAttributes.frame { return false }
	...
	return true
}

This works fairly well, until you add multi-line labels.


Here’s a simple setup with such label, a section footer reusable view:

Number of lines is set to 0, constraints are setup so that label fits-in inside its parent.

What I expect to happen is for the label to break into two lines but instead it stays in one line and adds the ellipsis at the end.

A quick look with Reveal app reveals that intrinsic content size of the label is too big:

It should be 414 (iPhone Xr portrait width) minus some custom margins.

Why this happens? At the moment UILabel is laying out its content (the text itself) it does not know what its frame will be, so it does not know how wide one line should be. Hence it draws entire text into one single line. To solve this, Apple added preferredMaxLayoutWidth property to UILabel. Thus, in the reusable view subclass, you need to set that value to the value of the label’s given width at the correct moment – before the view will perform the layout pass:

override func updateConstraints() {
	textLabel.preferredMaxLayoutWidth = textLabel.bounds.width
	super.updateConstraints()
}

This results in desired output: