How does Auto Layout work, and what are intrinsic size, content hugging, compression resistance, priorities and ambiguity?

9.1.1 · L2 · Interview

Predict first

Does a plain UIView have an intrinsic size, and which of two labels stretches when one has a higher hugging priority?

import UIKit

let w = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
let root = UIView(frame: w.bounds)
w.addSubview(root)
print(UIView().intrinsicContentSize == CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric))
let a = UILabel(), b = UILabel()
a.text = "A"; b.text = "B"
for l in [a, b] { l.translatesAutoresizingMaskIntoConstraints = false; root.addSubview(l) }
NSLayoutConstraint.activate([
    a.leadingAnchor.constraint(equalTo: root.leadingAnchor), b.leadingAnchor.constraint(equalTo: a.trailingAnchor),
    b.trailingAnchor.constraint(equalTo: root.trailingAnchor),
    a.topAnchor.constraint(equalTo: root.topAnchor), b.topAnchor.constraint(equalTo: root.topAnchor)])
a.setContentHuggingPriority(.defaultLow, for: .horizontal)
b.setContentHuggingPriority(.defaultHigh, for: .horizontal)
root.layoutIfNeeded()
print(a.frame.width > 200, b.frame.width < 30)
  • true, then true true
  • false, then true true
  • true, then false false

Predict first

A required width of 100 and a 999-priority width of 200 on the same view. What width results?

import UIKit

let w = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
let root = UIView(frame: w.bounds)
w.addSubview(root)
let box = UIView(); box.translatesAutoresizingMaskIntoConstraints = false; root.addSubview(box)
let c1 = box.widthAnchor.constraint(equalToConstant: 100)
let c2 = box.widthAnchor.constraint(equalToConstant: 200); c2.priority = UILayoutPriority(999)
NSLayoutConstraint.activate([c1, c2, box.heightAnchor.constraint(equalToConstant: 10),
    box.leadingAnchor.constraint(equalTo: root.leadingAnchor), box.topAnchor.constraint(equalTo: root.topAnchor)])
root.layoutIfNeeded()
print(box.frame.width)
  • 200.0
  • 100.0
  • 150.0

Predict first

A view has width, height and a leading constraint, but nothing fixes its vertical position. Is the layout ambiguous?

import UIKit

let w = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
let root = UIView(frame: w.bounds)
w.addSubview(root)
let v = UIView(); v.translatesAutoresizingMaskIntoConstraints = false; root.addSubview(v)
v.widthAnchor.constraint(equalToConstant: 50).isActive = true
v.heightAnchor.constraint(equalToConstant: 50).isActive = true
v.leadingAnchor.constraint(equalTo: root.leadingAnchor).isActive = true
root.layoutIfNeeded()
print(v.hasAmbiguousLayout)
  • true
  • false

Mental model. Each constraint is a rule in a seating plan: “B sits right of A”, “the table is at least 100 wide”. The solver finds a seating that obeys the strict rules and, where it can, the soft ones. Too few rules and people can sit anywhere.

Auto Layout is a constraint solver (a Cassowary-style linear-equation system). Each constraint is a rule of the form item1.attribute = multiplier × item2.attribute + constant (or an inequality), with a priority from 1 to 1000, where 1000 is required. The engine finds frames that satisfy all required constraints and as many optional ones as it can. A view needs enough constraints to fix x, y, width and height, and a view with translatesAutoresizingMaskIntoConstraints true brings its autoresizing mask in as constraints, which is why code-built views need that flag turned off.

Some views know their natural size. Intrinsic content size (intrinsicContentSize) is what a label, button or image wants given its content; a plain UIView has none (noIntrinsicMetric). The solver turns it into two low-priority constraints per axis: content hugging (“do not grow larger than my content”, default 250) and compression resistance (“do not shrink below my content”, default 750). When two views share a row and there is spare space, the one with the lower hugging priority stretches, so the one with the higher priority stays at its content size; when space is short, the lower compression-resistance priority is squeezed first. That is how you say which label truncates.

Two failures show up in the console. Unsatisfiable constraints: two required constraints disagree; the engine logs the set, breaks one and carries on (it does not crash); fix it by lowering one priority or removing a duplicate. Ambiguous layout: not enough constraints, so more than one solution exists and the result may differ by device or pass; check hasAmbiguousLayout and the view debugger. Debug with constraint.identifier and the Debug View Hierarchy. Prefer UIStackView and layout guides over many hand-written constraints, change constraints by activating or deactivating them (or updating constant) instead of recreating them, and use systemLayoutSizeFitting to measure a view’s size for a given width.

A plain view has no intrinsic size. The label with the lower hugging priority (a) takes the spare width, and the higher-priority one stays at its content width.

The required (1000) constraint wins over the 999 one. The lower one is simply not satisfied; it is not an error, because it was optional.

The vertical position has no rule, so more than one solution exists. hasAmbiguousLayout reports it, and the result may vary.

Common traps

  • Forgetting translatesAutoresizingMaskIntoConstraints = false. The old mask constraints then conflict with yours.
  • Giving two required constraints that cannot both hold. The runtime breaks one arbitrarily, so the result depends on which; set the weaker to a priority below 1000.

They might follow up with

  • Which of two labels truncates? The one with the lower compression-resistance priority.
  • How do you animate a constraint change? Update the constant or active state, then call layoutIfNeeded() inside the animation block.

Related questions

  • 9.1.2 What is the difference between layoutSubviews and draw(_:), and what triggers each?
  • 7.2.2 What is the difference between frame and bounds, and what happens to them under a transform?
  • 8.5.1 How does SwiftUI’s layout negotiation work, and why do frame and padding order change the result?

What is the difference between layoutSubviews and draw(_:), and what… →