4.3 — Auto Layout & constraints

Opening scenario

A junior PR lands on your desk. A single screen, 340 lines of constraint code, four nested UIStackViews, six priority = 999 constraints to silence warnings, a if traitCollection.horizontalSizeClass == .compact block that no longer matches reality, and one // FIXME: Auto Layout is broken here comment from 2021. The screen looks fine on iPhone 15. It explodes on iPad in landscape with the keyboard up.

Auto Layout is not the enemy. Auto Layout misused is the enemy. This chapter is the playbook for using it without ending up in the world of 340-line constraint hell.

ToolWhen to reach for it
NSLayoutAnchorDefault. Modern, type-safe, readable.
UIStackViewWhenever you’d write 4+ constraints for sibling alignment.
NSLayoutConstraint.activate([...])Batch activation; faster than per-constraint isActive = true.
Visual Format LanguageAlmost never anymore. Legacy code only.
translatesAutoresizingMaskIntoConstraints = falseOn every view you add programmatically. Forget once, layout breaks silently.

Concept → Why → How → Code

What Auto Layout actually does

Auto Layout is a constraint solver. You declare relationships:

cardA.leadingAnchor == container.leadingAnchor + 16 cardA.widthAnchor == container.widthAnchor / 2 - 16 cardA.topAnchor == container.safeAreaLayoutGuide.topAnchor + 12

The engine (Cassowary algorithm) solves the system and assigns each view a frame. Per frame of animation, per rotation, per Dynamic Type change — solved fresh.

The cost: solving is non-trivial. For 50 views with reasonable constraints, ~1ms on modern hardware. For 500 nested views with conflicting priorities, several frames. Profile if your scroll stutters.

NSLayoutAnchor — your only constraint API in 2026

The modern way. Type-safe (you can’t constrain leadingAnchor to topAnchor — won’t compile):

let card = UIView()
card.translatesAutoresizingMaskIntoConstraints = false   // ← forget this and you'll hate yourself
view.addSubview(card)

NSLayoutConstraint.activate([
    card.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
    card.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
    card.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
    card.heightAnchor.constraint(equalToConstant: 120),
])

The anchors:

  • Edge: leadingAnchor, trailingAnchor, topAnchor, bottomAnchor, leftAnchor, rightAnchor
  • Center: centerXAnchor, centerYAnchor
  • Dimension: widthAnchor, heightAnchor
  • Baseline (text views): firstBaselineAnchor, lastBaselineAnchor

Always use leading/trailing, not left/right. Leading/trailing flip for RTL languages (Arabic, Hebrew) automatically; left/right do not.

UIStackView — the single highest-leverage view

90% of layouts you’d write 4 constraints for, you can write 1 stack view for:

let stack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel, actionButton])
stack.axis = .vertical
stack.spacing = 12
stack.alignment = .leading      // .leading, .center, .trailing, .fill
stack.distribution = .fill      // .fill, .fillEqually, .fillProportionally, .equalSpacing, .equalCentering
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)

NSLayoutConstraint.activate([
    stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
    stack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
])

That’s 3 constraints for 3 stacked subviews. Without stack view: 9+ constraints.

Stack view properties to know:

  • axis: .vertical or .horizontal
  • spacing: gap between arranged subviews
  • alignment: cross-axis alignment of arranged subviews
  • distribution: how arranged subviews share the main axis
  • setCustomSpacing(_:after:): per-pair spacing override (iOS 11+)
  • isLayoutMarginsRelativeArrangement: respects layoutMargins

Nest stack views for grids:

let row = UIStackView(arrangedSubviews: [cellA, cellB, cellC])
row.axis = .horizontal
row.distribution = .fillEqually
row.spacing = 8

let grid = UIStackView(arrangedSubviews: [row, anotherRow])
grid.axis = .vertical
grid.spacing = 8

For dense grids prefer UICollectionView. For UIs with 2-4 sections of stacked content, nested stacks are clean.

Priorities & content hugging / compression

Every constraint has a priority (1-1000, default 1000 = required). When constraints conflict, the lower priority loses.

let optional = label.widthAnchor.constraint(equalToConstant: 200)
optional.priority = .defaultLow  // 250
optional.isActive = true

Two implicit priorities every view has:

  • Content hugging priority: “how strongly do I resist being stretched larger than my intrinsic size?”
  • Content compression resistance priority: “how strongly do I resist being squeezed smaller than my intrinsic size?”

Example: two labels side by side, one long, one short. Without tuning, Auto Layout doesn’t know which to truncate.

// "Always show me fully; truncate the other one"
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
detailLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

Common pattern with a label + chevron in a row:

// Title takes whatever space is left after the chevron
titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
chevronImageView.setContentHuggingPriority(.required, for: .horizontal)

Intrinsic content size

Some views know their natural size:

  • UILabel: size of its text in its font
  • UIImageView: image dimensions
  • UIButton: title + image + insets
  • UISwitch, UITextField: fixed system sizes

Custom views override:

override var intrinsicContentSize: CGSize {
    CGSize(width: 200, height: 44)
}

// Call when intrinsic size changes
invalidateIntrinsicContentSize()

For views with intrinsic size, you don’t need width/height constraints; Auto Layout uses the intrinsic size. That’s why stack views of labels “just work.”

Safe area, layout margins, readable content

Three guides you’ll reference:

view.safeAreaLayoutGuide       // avoid notch, home indicator, status bar
view.layoutMarginsGuide        // configurable insets (system default 8-20pt)
view.readableContentGuide      // width-capped guide for readable text on iPad

For typical screens, constrain to safeAreaLayoutGuide. For text-heavy screens (article reader), constrain to readableContentGuide so text doesn’t span 1024pt on iPad.

articleLabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
articleLabel.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),

iPad in landscape, readable content guide caps at ~672pt with auto margins.

Size classes & trait collections

Two size classes (compact, regular) for each axis. Combinations:

ClassDevices
Compact width, Regular heightiPhone portrait
Regular width, Regular heightiPad full screen, iPhone Plus landscape
Compact width, Compact heightiPhone landscape
Regular width, Compact heightiPad in split view (sometimes), iPhone Pro Max landscape

Adapt layout in traitCollectionDidChange:

override func traitCollectionDidChange(_ previous: UITraitCollection?) {
    super.traitCollectionDidChange(previous)
    if traitCollection.horizontalSizeClass == .regular {
        stack.axis = .horizontal
    } else {
        stack.axis = .vertical
    }
}

In iOS 17+ prefer the trait change registration API:

registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: ContentVC, _) in
    self.updateLayoutForSizeClass()
}

Animating constraints

You can animate constraint changes, not frame changes (with Auto Layout):

heightConstraint.constant = 200   // change the constraint, not the frame

UIView.animate(withDuration: 0.3) {
    self.view.layoutIfNeeded()      // forces layout pass *inside* animation block
}

The pattern:

  1. Update constraint constants
  2. Call layoutIfNeeded() inside an UIView.animate block on the root of the affected hierarchy
  3. Auto Layout resolves new positions; animation interpolates between old and new frames

Debugging Auto Layout

The console will yell at you with “Unable to satisfy constraints”:

2026-05-18 14:32:11.044 MyApp[1234:5678] [LayoutConstraints] Unable to simultaneously satisfy constraints.
  Probably at least one of the constraints in the following list is one you don't want.
    ...
  Will attempt to recover by breaking constraint
    <NSLayoutConstraint:0x... UIView.height == 100>

Read the list carefully — usually two constraints disagree (a fixed height of 100 plus content too tall for 100). Fix:

  • Remove one of the conflicting constraints, or
  • Lower the priority of the optional one, or
  • Use >= instead of == for flexible bounds

In Xcode, set the Symbolic Breakpoint UIViewAlertForUnsatisfiableConstraints to break exactly when the issue happens, with full stack trace.

For runtime debugging:

po view.constraintsAffectingLayout(for: .horizontal)
po view.hasAmbiguousLayout
po view.exerciseAmbiguityInLayout()   // animates between possible layouts

Performance rules

Auto Layout is fast for typical screens, slow for pathological cases:

  • Avoid deep nesting (10+ levels). Each level is a solver step.
  • Activate constraints in batches with NSLayoutConstraint.activate([...]); faster than per-constraint isActive = true.
  • Don’t deactivate and reactivate the same constraints per frame. Cache constraint references; toggle isActive.
  • Pre-size UIStackView with setContentCompressionResistancePriority to avoid ambiguous fallbacks.
  • UICollectionViewCompositionalLayout uses Auto Layout under the hood; profile with Instruments’ “Hangs” tool if you see scroll jank.

Cells & self-sizing

UITableViewCell and UICollectionViewCell can self-size via Auto Layout:

tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 80   // hint for scrollbar accuracy

// In cell:
override func awakeFromNib() {
    super.awakeFromNib()
    contentView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
        contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
        contentView.topAnchor.constraint(equalTo: topAnchor),
        contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
    ])
    // Cell internals: constrain subviews to contentView.
    // CRITICAL: subviews must form a constraint chain from top to bottom
    // so the cell can compute its own height.
}

Common bug: chain breaks (a subview is constrained to the top but not the bottom), so the cell collapses to estimatedRowHeight and stays there. Always verify your subview constraints form a top-to-bottom and leading-to-trailing chain.

When to abandon Auto Layout

Two cases:

  1. Custom layout passes for highly dynamic UIs: A magazine layout with flowing text, image breakouts, dynamic line breaks. Use UICollectionViewCompositionalLayout (still Auto Layout-aware) or manual layoutSubviews.
  2. Performance-critical animations: A 120fps chart cursor that follows pan gestures. Use CATransform3D or direct frame manipulation in a view that doesn’t participate in Auto Layout (set translatesAutoresizingMaskIntoConstraints = true, no constraints).

For 99% of UI, Auto Layout is the right tool.

In the wild

  • Apple’s UIKit Catalog sample ships dozens of UIStackView examples; the canonical reference.
  • Airbnb’s Epoxy uses UIStackView internally; their declarative views compile down to nested stacks plus constraints.
  • Twitter (now X) famously rewrote their feed with UIStackViews in 2017 and shaved 40% off layout time vs hand-rolled constraints (per their engineering blog).
  • iOS Mail’s message list uses self-sizing UITableViewCell with stack views — long subjects expand row height naturally.

Common misconceptions

  1. “Auto Layout is slow.” Misused Auto Layout is slow. Used correctly, plenty fast for most apps.
  2. UIStackView is just sugar.” It’s a real UIView subclass that manages its own constraints. Costs the same as nested stack-of-views with no view of your own.
  3. “Set translatesAutoresizingMaskIntoConstraints = false always.” Only on views you constrain. Views you frame manually keep it true. Cells’ contentView is true by default and should remain so unless you specifically need its constraints.
  4. “Priority 999 vs 1000 doesn’t matter.” It matters a lot. 999 is “I’d really like this but I’ll yield”; 1000 is “I will crash before yielding.” The difference avoids most constraint-conflict warnings.
  5. “Constraints set in viewDidLoad are enough.” Constraints between views in different VCs (e.g., child VC’s view to parent’s view) must be set after containment is established and before viewWillLayoutSubviews. Get the lifecycle wrong, layout breaks.

Seasoned engineer’s take

Auto Layout mastery is mostly knowing when to reach for UIStackView vs raw constraints. The rule I use:

  • Layout has visible “flow” (top to bottom or left to right with predictable spacing) → UIStackView
  • Layout has overlapping elements, precise asymmetric positioning, or per-view animation → raw NSLayoutAnchor
  • Layout is a grid or list of repeating items → UICollectionView with compositional layout

You should be able to look at any iPhone screen and sketch its hierarchy and stack-view structure in 60 seconds. That’s the bar.

Three habits:

  1. Always activate constraints in batches. NSLayoutConstraint.activate([...]), never one-at-a-time.
  2. Name constraints you’ll animate. Stuff them in instance properties so you can mutate .constant later instead of removing/recreating.
  3. Test in the simulator at every size class. iPhone 16, iPhone 16 Pro Max landscape, iPad Air, iPad Pro 13“ split view, Mac Catalyst window resize. Each surfaces different bugs.

TIP: When debugging “why isn’t this label showing up?”, check four things in this order: (1) was it added to a superview? (2) is translatesAutoresizingMaskIntoConstraints = false? (3) does it have constraints in both X and Y axes? (4) is the color the same as the background?

WARNING: Animating with view.layoutIfNeeded() outside the right context (e.g., on a subview rather than the root) may animate nothing — or animate too much. Always call it on the common ancestor of the views whose layout changes.

Interview corner

Junior-level: “How do you pin a view to the safe area of its superview?”

NSLayoutConstraint.activate([
    v.leadingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leadingAnchor),
    v.trailingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.trailingAnchor),
    v.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor),
    v.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor),
])

And don’t forget v.translatesAutoresizingMaskIntoConstraints = false.

Mid-level: “What’s the difference between content hugging and compression resistance?”

Content hugging: how strongly a view resists growing past its intrinsic size. Content compression resistance: how strongly it resists shrinking below its intrinsic size. Tune them when two views share space and only one should yield (e.g., a label next to a chevron).

Senior-level: “Design a chat bubble row that auto-sizes to text, has a max-width, and aligns left or right based on sender.”

Cell with a horizontal UIStackView containing a bubble view. Bubble view contains a UILabel with numberOfLines = 0, preferredMaxLayoutWidth set in layoutSubviews (or use compositional layout’s widthDimension). Max width constraint on the bubble at high priority (.required - 1), leading or trailing alignment via toggling stack view’s alignment or by inserting UIView() spacers. Self-sizing rows via tableView.rowHeight = .automaticDimension. For iMessage-style elasticity, swap to compositional layout with estimated heights.

Red flag in candidates: Setting frame manually inside a view that already has constraints. Means they don’t understand the contract.

Lab preview

Auto Layout shows up in Lab 4.1 (list with self-sizing cells), Lab 4.2 (compositional layout), and Lab 4.3 (form layout with stack views).


Next: 4.4 — Navigation