Auto Layout is a constraint-based layout system: you describe relationships between views ("16 pt below the title, as wide as the card"), and a solver turns them into frames on every layout pass. With frame-based layout you compute and assign every rectangle yourself.
Adapts on its own: rotation, iPad window resizing, Dynamic Type, longer translations and right-to-left languages need no new code, because the solver recomputes the same constraints; frame code has to recompute positions in layoutSubviews() for each case.
Content-driven: labels and buttons report an intrinsic content size, so the length of the text can drive the height without measuring strings by hand.
Cost: the solver (based on the Cassowary algorithm) does more work than assigning a frame. It is fast enough for ordinary screens; hand-written frames still win in very hot paths, such as complex cells in a huge feed.
Mixing is allowed: a frame-based view can live inside a constraint-based parent. What you cannot do is drive the same view with both.
In code you create constraints with layout anchors or let UIStackView generate them; in Interface Builder you draw them.
iOSJunior
Auto Layout & UIKit Layout
Constraints, intrinsic content size, UIStackView, frames, safe area
Questions closed
0 / 25
- 0Review
- 3Asked often
Questions
Every constraint is a linear equation item1.attribute = multiplier × item2.attribute + constant, where the relation can also be ≤ or ≥. Layout anchors (topAnchor, widthAnchor, …) are the type-safe way to build that equation.
Attributes: leading/trailing, top/bottom, width/height, centerX/centerY, firstBaseline/lastBaseline.
Type safety: an anchor is an NSLayoutXAxisAnchor, NSLayoutYAxisAnchor or NSLayoutDimension, so relating a top to a leading does not compile. The original NSLayoutConstraint(item:attribute:relatedBy:toItem:attribute:multiplier:constant:) initializer accepts it and fails at runtime.
Multiplier: anchors offer it on dimensions (widthAnchor.constraint(equalTo:multiplier:)), which covers the usual "half the width" case.
**Sign of constant**: it is added to the second item, so trailing and bottom insets are negative: constant: -16.
Enough to fix four values (x position and width, y position and height), which usually means four constraints, two per axis. A view with an intrinsic content size, such as a UILabel, supplies its own size, so two positional constraints are enough.
Any pair that pins down an axis works: leading + trailing, leading + width, or centerX + width.
Too few: the layout is ambiguous: the solver picks some answer, and the view may jump or disappear. Interface Builder flags missing constraints; at runtime nothing is logged, only the View Debugger reports it.
Contradictory: the layout is unsatisfiable: the solver breaks one constraint and prints "Unable to simultaneously satisfy constraints".
card still has translatesAutoresizingMaskIntoConstraints set to true, so UIKit turns its frame (.zero) and autoresizing mask into constraints that contradict yours. Set card.translatesAutoresizingMaskIntoConstraints = false before activating the constraints.
With the flag on, UIKit adds NSAutoresizingMaskLayoutConstraint objects that pin the view to its current frame. Seeing that class name in the "Unable to simultaneously satisfy constraints" log is the tell.
The flag defaults to true for views created in code. Interface Builder sets it to false for you, and UIStackView does it for its arranged subviews.
Leave it alone on views UIKit positions: a view controller's root view, a cell's contentView, and any view you lay out with frames.
The autoresizing mask itself (springs and struts: .flexibleWidth, .flexibleHeight, …) is the older frame-based system. It still works for simple frame layout, just not combined with your own constraints on the same view.
It activates the constraints before badge is in the view hierarchy. Activation installs a constraint on the nearest common ancestor of both views, and a view without a superview has none, so UIKit throws. Call card.addSubview(badge) first, then activate.
Creating constraint objects early is fine; only activation walks the hierarchy.
Views in different hierarchies can never be constrained together: for example, a view inside a child view controller that you have not added yet.
removeFromSuperview() deactivates every constraint that mentions the view. If you add the view back later, activate the stored constraints again.
A quiet relative in cells: constraining to the cell instead of its contentView does not crash, but lays the content out against the wrong box.
The safe area is the part of a view not covered by the status bar, navigation and tab bars, the home indicator or the Dynamic Island. Pin content to view.safeAreaLayoutGuide and it stays visible on every device and in every orientation.
safeAreaLayoutGuide is a layout guide you constrain to; safeAreaInsets gives the same information as numbers, for frame-based code.
Backgrounds and images that should reach the screen edges pin to the view itself; text and buttons pin to the safe area.
additionalSafeAreaInsets on a view controller shrinks the safe area for its content, for example above a custom floating player bar.
UIScrollView applies the safe area through adjustedContentInset: content scrolls under the bars but starts and ends clear of them. With the floating Liquid Glass bars of iOS 26, that is the intended look.
safeAreaInsetsDidChange() is the callback for frame-based layout when the insets change.
Leading and trailing follow the reading direction, so the layout mirrors automatically in Arabic and Hebrew; left and right are physical sides and never flip.
The same split exists for insets: NSDirectionalEdgeInsets (leading/trailing) mirrors, UIEdgeInsets (left/right) does not.
Use left and right only for things that must not mirror. For a whole control such as media playback buttons, set semanticContentAttribute = .playback or .forceLeftToRight instead.
Directional images, such as a back chevron, need imageFlippedForRightToLeftLayoutDirection() or the Direction setting in the asset catalog; SF Symbols like chevron.backward flip on their own.
Check it by running with the scheme's Right-to-Left Pseudolanguage rather than waiting for a real translation.
UIStackView arranges its subviews in a row or a column and creates the constraints between them for you: you set axis, distribution, alignment and spacing, and constrain only the stack itself.
Less to write: no constraints between siblings, so fewer to get wrong.
Show and hide for free: isHidden = true on an arranged subview removes its slot and closes the gap; doing that by hand means swapping constraints.
Composable: nested vertical and horizontal stacks cover most cards and forms; setCustomSpacing(_:after:) handles a single different gap.
Drawback: depth: every stack is a view plus constraints, so deep nesting costs layout time in cells of long lists.
Drawback: limited shapes: overlapping views or aligning items across two different stacks still need ordinary constraints.
Gotcha: use addArrangedSubview(_:); a view added with addSubview(_:) is not arranged. When a stack row looks wrong, the fix is usually a hugging or compression priority on an arranged subview, not a new constraint.
distribution sizes views along the axis and alignment positions them across it: the equal widths come from distribution = .fillEqually, the stretched icon from the default alignment = .fill.
.fill (the default distribution) keeps intrinsic sizes and stretches the view with the lowest content hugging priority; on a tie the stack view picks by position in arrangedSubviews, so set the hugging priorities explicitly.
.fillEqually gives every view the same size and ignores intrinsic size; right for a Cancel/Confirm pair, wrong for an icon–title–chevron row.
.fillProportionally splits space in proportion to intrinsic sizes; .equalSpacing and .equalCentering keep natural sizes and spread the leftover space between views.
alignment = .fill stretches every view to the stack's height; .center, .top and .bottom keep each view's own height, and .firstBaseline lines up text in a horizontal stack.
Intrinsic content size is the size a view wants from its own content: a label's text, a button's title plus insets. Content hugging resists growing beyond it, compression resistance resists shrinking below it, and the view with the lower priority gives way first.
Auto Layout turns the intrinsic size into width and height constraints; hugging and compression resistance are simply the priorities of those constraints.
Defaults for views created in code: hugging 250 (.defaultLow), compression resistance 750 (.defaultHigh). Interface Builder gives labels a hugging of 251.
Two labels in one row with default priorities tie, so it is undefined which one stretches or truncates. Break the tie explicitly.
Typical row: the title should stretch and truncate, the badge should keep its natural size, so the title gets the lower hugging and the lower compression resistance.
Priorities are set per axis, .horizontal and .vertical separately.
A plain UIView has no intrinsic size and ignores both settings; it needs explicit size constraints, or a custom view overrides intrinsicContentSize.
@IBInspectable exposes a custom view's property in Interface Builder's Attributes inspector, and @IBDesignable makes Interface Builder build the view and render it on the canvas. Today UIKit views are usually previewed with the #Preview macro (Xcode 15+) instead.
@IBInspectable works for simple types: Bool, numbers, String, CGPoint/CGSize/CGRect, UIColor, UIImage. Interface Builder stores the value as a user-defined runtime attribute and sets it through key-value coding when the nib loads.
@IBDesignable compiles your module and runs it in a separate agent process to draw the canvas. It is slow, breaks whenever that build fails, and surfaces as "Failed to render and update auto layout status" errors; prepareForInterfaceBuilder() is where design-time-only setup goes.
#Preview renders a UIView or UIViewController built in code (UIKit previews need iOS 17+), so it also works for screens without storyboards.
A priority from 1 to 1000 says how hard the solver tries to satisfy a constraint: 1000 (.required) must hold, anything lower is optional: the solver satisfies it as closely as it can and gives way without a log message when it conflicts with something stronger. 999 means "as strong as possible, but allowed to yield".
Required conflicts are errors: the engine breaks a constraint of its own choosing and logs "Unable to simultaneously satisfy constraints". An optional constraint that loses produces no log.
Transient conflicts: a self-sizing cell is first laid out at a temporary required height (UIView-Encapsulated-Layout-Height); a bottom constraint at 999 yields for that pass instead of breaking something.
Changing it later: you can move between optional values on an active constraint, but switching to or from .required on an installed constraint throws. 999 keeps that option open.
Ranking against built-ins: compression resistance is 750 and hugging 250, so a 999 width constraint beats a label's text, while one at 500 loses to compression resistance when it asks for less width than the text needs.
Named levels: .required 1000, .defaultHigh 750, .defaultLow 250, .fittingSizeLevel 50; arithmetic works, as in .defaultHigh + 1.
didSet never calls invalidateIntrinsicContentSize(). Auto Layout caches the intrinsic size and re-reads it only after that call; setNeedsDisplay() just repaints the same rectangle.
The fix: didSet { invalidateIntrinsicContentSize(); setNeedsDisplay() }, ideally guarded by count != oldValue.
UILabel, UIButton and UIImageView invalidate themselves when their text or image changes, which is why the bug appears only in views you wrote.
Return UIView.noIntrinsicMetric for an axis with no natural size: a divider has a height but no width.
sizeThatFits(_:) is the frame-layout counterpart used by sizeToFit(). Auto Layout never calls it, so a view that implements only that one has no size under constraints.
Hugging and compression resistance act on the intrinsic size, so they only start working once the view reports a correct one.
The edge is the view's literal bounds, layoutMarginsGuide is the bounds minus the view's margins, and readableContentGuide additionally caps the width so lines of text stay a comfortable length on wide screens.
Margins are set with directionalLayoutMargins, which mirrors in right-to-left languages. A view controller's root view enforces a system minimum; viewRespectsSystemMinimumLayoutMargins = false lets you go narrower.
readableContentGuide is noticeably narrower than the view on iPad: body text pins to it, while a full-bleed image still pins to the edges.
safeAreaLayoutGuide answers a different question: what the bars do not cover. By default margins are measured from the safe area (insetsLayoutMarginsFromSafeArea), so pinning to margins also keeps you clear of the notch.
UIStackView ignores its own margins until you set isLayoutMarginsRelativeArrangement = true.
A plain UILayoutGuide is an invisible rectangle you can constrain to. Use it instead of a transparent spacer view: nothing to draw, no touch handling, no accessibility element.
setNeedsLayout() marks a view as needing layout in the next update cycle, layoutIfNeeded() performs any pending layout right now, and layoutSubviews() is the method UIKit calls during that layout; you call the first two and override the third.
setNeedsLayout() returns immediately and is cheap: ten calls in one run-loop turn still produce one layout pass.
layoutIfNeeded() runs synchronously for the view and its subtree. Use it when you need the new frames now, mainly inside a UIView.animate block after changing constraints, so the change animates instead of snapping: set heightConstraint.constant, then call layoutIfNeeded() inside the animation block.
layoutSubviews() is never called directly. Override it for what constraints cannot express: a CAGradientLayer frame, a shadow path, a corner radius derived from the final height. Call super first; that is where Auto Layout applies the solved frames.
Related pair: setNeedsUpdateConstraints() / updateConstraints() runs before layout, for constraint changes that depend on several properties at once.
Change the constant when only a number changes (it animates cleanly), swap active constraint sets when the relationships themselves change, and change priorities when two competing constraints should take turns winning.
**constant**: keep a reference to the constraint, assign the new value, then call layoutIfNeeded() inside an animation block if it should animate.
Activation: create both sets once, then NSLayoutConstraint.deactivate(old) followed by activate(new). Deactivate first, or the two sets briefly conflict and the log fills up.
Priority: two constraints at 750 and 250; swapping their priorities moves the view without deactivating anything. Stay within optional values: switching to or from .required on an installed constraint throws.
**isHidden in a stack view**: for plain show and hide, setting it on an arranged subview beats all three.
The bug worth naming is creating new constraints on every update instead of reusing stored ones. The old ones stay active, layout gets slower, and eventually two of them contradict each other. The smell is a constraint(equalTo:) call that runs on every state change and never deactivates the previous set.
layoutIfNeeded() is called on panel, but the changed constraint positions panel inside view, so the block moves nothing. The panel jumps on the next layout pass, outside the animation, instead of sliding up.
Why: layoutIfNeeded() lays out the subtree of the receiver: it places panel's subviews, while panel's own frame is set when its superview lays out. A constraint between panel and view belongs to their common ancestor, view.
The fix: call self.view.layoutIfNeeded() in the block: the solver applies the new constant there, the frame changes inside the animation and UIKit adds a position animation to the layer.
Rule: call it on the closest common ancestor of every view the changed constraints touch; for a view controller that is usually view.
Catch it: slow animations in the simulator (Debug ▸ Slow Animations) show the snap at once; in a test, check panel.layer.animationKeys() right after showPanel().
Read the log: it lists the set of constraints that cannot all hold and names the one it broke. Give your constraints identifiers so the log shows names, then use the UIViewAlertForUnsatisfiableConstraints breakpoint and the View Debugger to see which code and which views are involved.
Read the list: the last lines name the constraint the engine chose to break. NSAutoresizingMaskLayoutConstraint in the list means a forgotten translatesAutoresizingMaskIntoConstraints = false; UIView-Encapsulated-Layout-Height means a self-sizing cell (see the priorities question).
Identifiers: constraint.identifier = "card.bottom" replaces anonymous addresses in the log with a readable name.
Symbolic breakpoint on UIViewAlertForUnsatisfiableConstraints stops at the moment of the conflict, so the stack trace shows which code activated the bad constraint.
View Debugger (Debug View Hierarchy): select a view to see its constraints in the inspector; ambiguous layouts appear as purple runtime issues.
Ambiguity is separate: it produces no log at all; view.hasAmbiguousLayout in the debugger checks it.
Fix the cause: usually a duplicate constraint, a fixed height on content that should drive the height, or two required constraints that disagree. Lowering one to 999 is right only when the conflict is temporary by design.
There must be an unbroken chain of vertical constraints from the top of contentView to its bottom, so Auto Layout can compute the height; the table then uses UITableView.automaticDimension, which is already the default.
Every subview is linked into the chain: top pinned, each gap defined, the last view pinned to the bottom. A missing bottom constraint breaks the chain: the row gets a wrong height (often the 44 pt default) and the layout is ambiguous.
Constraints go to contentView, not to the cell itself.
A multi-line label needs numberOfLines = 0 and both horizontal edges pinned, because its height depends on its width.
No fixed height on anything whose content should drive the height.
A conflict with UIView-Encapsulated-Layout-Height on first layout is the cell's temporary height; a 999 priority on the bottom constraint lets it yield quietly.
Since iOS 16, selfSizingInvalidation (enabled by default) lets a cell resize after its content changes (call invalidateIntrinsicContentSize() in the cell) without reloading the row.
Collection views work the same way, with the estimate set in the layout: .estimated(88) in a compositional layout.
A scroll view computes its content size from its content's constraints, so pin the content to all four edges of contentLayoutGuide and take its width from frameLayoutGuide.
contentLayoutGuide describes the scrollable area; constraints to it determine contentSize.
frameLayoutGuide describes the scroll view's visible frame; constraints to it determine what stays fixed on screen.
The recipe: pin a single container or stack view to all four edges of the content guide, then set its width equal to frameLayoutGuide.widthAnchor for vertical scrolling (height for horizontal).
That width constraint is the step people skip: without it the container's width is unconstrained, so the scroll view shows nothing or scrolls sideways.
Inside the container you still need a full top-to-bottom chain, exactly as in a self-sizing cell; a vertical UIStackView gives you that for free.
Constrain the field's bottom to view.keyboardLayoutGuide.topAnchor. Since iOS 15 that guide tracks the keyboard in your view's coordinates and animates with it, so one constraint replaces the whole notification pipeline.
With the keyboard hidden the guide sits at the bottom of the safe area, so the same constraint works in both states.
followsUndockedKeyboard = true also tracks a floating or split iPad keyboard; with the default false the guide ignores an undocked keyboard.
usesBottomSafeArea = false (iOS 17) lets the guide reach the view's bottom edge instead of stopping at the safe area.
The guide moves only what is constrained to it: pin the scroll view's bottom to it too, or the last fields stay hidden.
The old approach observed keyboardWillShow/keyboardWillHide and computed view.bounds.height - keyboardFrame.height. That misses every frame change in between (the predictive bar, the emoji keyboard, a hardware keyboard), and the rect from keyboardFrameEndUserInfoKey is in screen coordinates, which is wrong in Split View, Slide Over and Stage Manager unless you convert it into the view. Observing keyboardWillChangeFrameNotification and converting the rect was always the correct form.
Size classes are coarse traits (.compact or .regular) for the width and height your view actually gets. Branch on traitCollection.horizontalSizeClass, not on the device: an iPad app in Split View or a narrow window gets a compact width.
Typical values: iPhone portrait is compact width, regular height; most iPhones in landscape are compact in both (the largest models get regular width); iPad full screen is regular in both.
UIDevice.current.userInterfaceIdiom is a hardware fact, not a layout fact, and UIScreen.main is deprecated since iOS 16 because a window is not the screen.
React with registerForTraitChanges([UITraitHorizontalSizeClass.self]) (iOS 17); traitCollectionDidChange(_:) is deprecated.
traitOverrides.horizontalSizeClass = .compact forces a size class on a child controller, which is useful for embedding and for snapshot tests of both layouts.
Interface Builder can also vary constraints and properties per size class.
Before building two columns by hand, check UISplitViewController: it collapses to one column at compact width and handles the navigation for you.
Mostly you do not: constraints relative to the parent and the safe area re-solve for the new size on their own. Code is needed only when the structure changes, and it belongs in viewWillTransition(to:with:) or a size-class handler, not in device-orientation notifications.
viewWillTransition(to:with:) gives you the new container size; make changes inside coordinator.animate(alongsideTransition:) so they move with the rotation instead of snapping at the end.
Decide by size or size class rather than orientation: an iPad window can be narrow in landscape, and windows resize without any rotation at all.
UIDevice.orientationDidChangeNotification reports how the device is held, including face up, and fires even when the interface does not rotate; it is not a layout signal.
Allowed orientations come from supportedInterfaceOrientations on the root or full-screen presented controller (and Info.plist). When that answer changes, call setNeedsUpdateOfSupportedInterfaceOrientations() (iOS 16).
To request an orientation, use windowScene.requestGeometryUpdate(.iOS(interfaceOrientations:)). The old UIDevice.current.setValue(_:forKey: "orientation") hack no longer works since iOS 16.
Something inside the layout pass invalidates layout again: constraints are changed from geometry that the same pass just produced, so every pass schedules the next one.
Usual shapes: setting a constraint's constant from a frame measured in layoutSubviews(), activating or deactivating constraints there, calling invalidateIntrinsicContentSize() unconditionally, or calling layoutIfNeeded() from inside a layout callback.
Oscillation: a subtler version flips between two states instead of diverging: a label's height changes the width available to it, which changes how it wraps, which changes its height again.
Cheap fix: a guard: assign only when the value really differs, so the second pass finds nothing to do. Compare floats with a tolerance, not ==.
Real fix: derive from data: drive constraint changes from the model, traits or user input, never from resulting frames. Work that legitimately follows geometry, such as a sublayer frame or a shadow path, is safe in layoutSubviews() because it touches no constraint.
Diagnose it: launch with -UIViewLayoutFeedbackLoopDebuggingThreshold 100. UIKit raises an exception with a diagnostic once one view lays out that many times in a single run-loop turn, which names the culprit quickly.
The model to keep in mind: a layout pass is a function from constraints to frames. Anything that writes its output back into its input makes it recursive.
Find out how much layout work each cell does per scroll step, then cut it: fewer views and constraints per cell, no constraint changes on reuse, and frames for the few hot cells that still cost too much.
Measure
Time Profiler: look for layoutSubviews, -[NSISEngine ...] and systemLayoutSizeFitting under the scroll; Instruments' Animation Hitches template shows which frames missed.
Count the views: the View Debugger shows the real depth; a cell with five nested UIStackViews is dozens of views and hundreds of constraints.
Cut the cost
Flatten: one stack or a few direct constraints instead of stacks inside stacks; hide with isHidden instead of removing and re-adding views.
Reuse constraints: build them once in the cell's init and change only constant or isActive in prepareForReuse() or the configure method; never create constraints per cellForRowAt.
Do not size by hand in hot paths: calling systemLayoutSizeFitting(_:) per row on the main thread repeats the solve; cache heights by model id, and give the table an accurate estimatedRowHeight so it does not measure every row up front.
Fall back to frames: a cell that is still too expensive can compute its frames in layoutSubviews() from a cached size; keep constraints for the rest of the screen.