How do you measure a view or a wrapped label: systemLayoutSizeFitting, sizeThatFits and preferredMaxLayoutWidth?

9.8.6 · L3 · Interview

Predict first

How do the fitting results behave for a wrapping label?

import UIKit

let l = UILabel(); l.numberOfLines = 0; l.font = .systemFont(ofSize: 17)
l.text = String(repeating: "word ", count: 20)
let narrow = l.systemLayoutSizeFitting(CGSize(width: 100, height: 0), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
let wide = l.systemLayoutSizeFitting(CGSize(width: 300, height: 0), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
let packed = l.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
print(narrow.height > wide.height, narrow.width <= 100, abs(packed.height - l.font.lineHeight) < 1, packed.width > 300)
print(l.sizeThatFits(CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude)).height == narrow.height)
print(l.intrinsicContentSize.width > 800)
l.preferredMaxLayoutWidth = 100
print(l.intrinsicContentSize.height > 150)
  • true true true true, true, true, true
  • true true false false, true, false, false
  • false true true true, true, true, true

Mental model. You hand the label a line length and ask how tall the paragraph will be; with no line length it writes one very long line.

Auto Layout can tell you the size a view would take: systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:) solves the view’s constraints for a target size. The usual recipe for self-sizing is to fix the width with .required and let the height be as small as the content allows with .fittingSizeLevel; UIView.layoutFittingCompressedSize asks for the smallest possible size on both axes (so a wrapping label becomes one very wide line). A multi-line UILabel needs to know its width to compute its height: its intrinsicContentSize uses preferredMaxLayoutWidth (0 means “no limit”), and Auto Layout normally sets that for you from the width the constraints give it. sizeThatFits(_:) is the older, non-constraint way: it asks the view itself for a size within a bound.

Results are fractional (a 100-point width fits a label into about 84.7 points because words break early), so round up to whole pixels (ceil) before using a value for a frame. Measuring text directly with boundingRect needs the same width and options as real layout (see the text-measurement question). Self-sizing cells and headers are this call under the hood: width fixed by the table, height from the content. Measure once per width and cache; solving is not free.

Narrower means taller; compressed means one line; the intrinsic size follows the preferred width.

Common traps

  • Asking a wrapping label for its fitting size without fixing the width. It answers with a single enormous line.
  • Using fractional sizes as frames. Round up to pixels to avoid blurry edges and clipped text.

They might follow up with

  • What does preferredMaxLayoutWidth do? It tells a multi-line label the width to wrap at when computing its intrinsic height.
  • Why is a cell’s height wrong on first load? The width was not yet final when it was measured.

Related questions

  • 9.1.1 How does Auto Layout work, and what are intrinsic size, content hugging, compression resistance, priorities…
  • 7.5.4 How do prefetching and self-sizing cells keep scrolling smooth, and what can still make it janky?
  • 9.11.3 How do you measure text correctly: font metrics, boundingRect options and rounding, and what paragraph styles…

← In what order do updateConstraints, layoutSubviews and the layout…   How do autoresizing masks work, how does the extra space get shared… →