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.
| Tool | When to reach for it |
|---|---|
NSLayoutAnchor | Default. Modern, type-safe, readable. |
UIStackView | Whenever you’d write 4+ constraints for sibling alignment. |
NSLayoutConstraint.activate([...]) | Batch activation; faster than per-constraint isActive = true. |
| Visual Format Language | Almost never anymore. Legacy code only. |
translatesAutoresizingMaskIntoConstraints = false | On 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 + 16cardA.widthAnchor == container.widthAnchor / 2 - 16cardA.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:.verticalor.horizontalspacing: gap between arranged subviewsalignment: cross-axis alignment of arranged subviewsdistribution: how arranged subviews share the main axissetCustomSpacing(_:after:): per-pair spacing override (iOS 11+)isLayoutMarginsRelativeArrangement: respectslayoutMargins
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 fontUIImageView: image dimensionsUIButton: title + image + insetsUISwitch,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:
| Class | Devices |
|---|---|
| Compact width, Regular height | iPhone portrait |
| Regular width, Regular height | iPad full screen, iPhone Plus landscape |
| Compact width, Compact height | iPhone landscape |
| Regular width, Compact height | iPad 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:
- Update constraint constants
- Call
layoutIfNeeded()inside anUIView.animateblock on the root of the affected hierarchy - 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-constraintisActive = true. - Don’t deactivate and reactivate the same constraints per frame. Cache constraint references; toggle
isActive. - Pre-size
UIStackViewwithsetContentCompressionResistancePriorityto avoid ambiguous fallbacks. UICollectionViewCompositionalLayoutuses 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:
- 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 manuallayoutSubviews. - Performance-critical animations: A 120fps chart cursor that follows pan gestures. Use
CATransform3Dor directframemanipulation in a view that doesn’t participate in Auto Layout (settranslatesAutoresizingMaskIntoConstraints = true, no constraints).
For 99% of UI, Auto Layout is the right tool.
In the wild
- Apple’s UIKit Catalog sample ships dozens of
UIStackViewexamples; the canonical reference. - Airbnb’s Epoxy uses
UIStackViewinternally; 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
UITableViewCellwith stack views — long subjects expand row height naturally.
Common misconceptions
- “Auto Layout is slow.” Misused Auto Layout is slow. Used correctly, plenty fast for most apps.
- “
UIStackViewis just sugar.” It’s a realUIViewsubclass that manages its own constraints. Costs the same as nested stack-of-views with no view of your own. - “Set
translatesAutoresizingMaskIntoConstraints = falsealways.” Only on views you constrain. Views you frame manually keep ittrue. Cells’contentViewistrueby default and should remain so unless you specifically need its constraints. - “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.
- “Constraints set in
viewDidLoadare 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 beforeviewWillLayoutSubviews. 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 →
UICollectionViewwith 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:
- Always activate constraints in batches.
NSLayoutConstraint.activate([...]), never one-at-a-time. - Name constraints you’ll animate. Stuff them in instance properties so you can mutate
.constantlater instead of removing/recreating. - 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