Auto Layout & UIKit Layout
Constraints, intrinsic content size, UIStackView, frames, safe area
- 01
What is Auto Layout? How is it different from frame-based layout?
EasyAuto Layout is a constraint-based layout engine.
let a = UIView(), b = UIView() [a, b].forEach { $0.translatesAutoresizingMaskIntoConstraints = false view.addSubview($0) } … - 02
What is intrinsic content size? How does it relate to compression resistance and hugging?
MediumA view's intrinsic content size is the size it wants to be based on its own content — a label sized to its text, a button to its title plus insets.
let title = UILabel(); title.text = "Long article title that wraps" let badge = UILabel(); badge.text = "NEW" // Title should yield space; badge should hold its size title.setContentHuggingPriority(.defaultLow, for: .horizontal) title.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal) … - 03
Why reach for UIStackView instead of writing constraints between siblings?
EasyUIStackView generates the constraints between its arranged subviews for you: you set an axis, a distribution and an alignment, then constrain only the stack itself.
// Vertical stack of content let content = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel, metadataRow, ]) … - 04
What is the safe area? How do you handle it in UIKit?
MediumSafe area is the region of the screen NOT covered by system UI: status bar, navigation bar, tab bar, home indicator, and notches/cutouts on modern iPhones.
// Pin a button to the bottom safe area (above the home indicator) view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), … - 05
How do you debug Auto Layout issues like 'unable to satisfy constraints'?
MediumWhen Auto Layout can't find a valid solution it prints a long diagnostic and breaks one constraint.
// Name constraints for readable logs let c = button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8) c.identifier = "buttonBottom" c.isActive = true // Make an optional constraint break gracefully … - 06
What is the difference between layoutIfNeeded, setNeedsLayout, and layoutSubviews?
MediumsetNeedsLayoutschedules a layout pass,layoutIfNeededruns one immediately, andlayoutSubviewsis the callback UIKit invokes during that pass — you call the first two and override the third.// Animate by changing constraint constants leadingConstraint.constant = 100 view.setNeedsLayout() UIView.animate(withDuration: 0.3) { view.layoutIfNeeded() // forces the new layout to apply inside the animation } … - 07
You added constraints in code but every view is stacked in the top-left corner — what did you forget?
EasyA view created in code still has
translatesAutoresizingMaskIntoConstraintsset totrue, which turns its frame and autoresizing mask into real constraints that fight the ones you wrote.let card = UIView() card.translatesAutoresizingMaskIntoConstraints = false // the line everyone forgets once view.addSubview(card) NSLayoutConstraint.activate([ card.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), card.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), … - 08
What has to be true for a table view cell to size itself to its content?
MediumA self-sizing cell needs an unbroken chain of vertical constraints from the top of
contentViewto its bottom, so Auto Layout can solve for the height instead of falling back on a fixed one.final class MessageCell: UITableViewCell { private let title = UILabel() private let body = UILabel() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) … - 09
How do you lay out content inside a UIScrollView with Auto Layout so it actually scrolls?
MediumA scroll view computes its content size from its subviews' constraints, so the content has to be pinned to
contentLayoutGuideon all four edges and take its width fromframeLayoutGuide.let scrollView = UIScrollView() let content = UIStackView() // one container, pinned to the content guide content.axis = .vertical content.spacing = 16 [scrollView, content].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } view.addSubview(scrollView) … - 10
A layout has to change when some state flips — do you change the constant, swap which constraints are active, or change priority?
MediumChange the
constantfor anything you would animate, swap active constraints for a structural change, and change priority only to decide which of two competing constraints wins.private var heightConstraint: NSLayoutConstraint! // keep the reference, change the constant func setExpanded(_ expanded: Bool, animated: Bool) { heightConstraint.constant = expanded ? 240 : 88 guard animated else { view.layoutIfNeeded(); return } UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() } … - 11
What is the difference between pinning to a view's edge, its
layoutMarginsGuide, and itsreadableContentGuide?EasyPinning to the edge gives you the literal bounds,
layoutMarginsGuideadds the view's own margins, andreadableContentGuidecaps the width so a paragraph does not run edge to edge on a large screen.// Three different edges to pin to, with three different meanings label.leadingAnchor.constraint(equalTo: view.leadingAnchor) // literal bounds label.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor) // plus margins label.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor) // readable width // Margins are directional, so they mirror in right-to-left languages … - 12
What breaks a UIKit layout in Arabic or at the largest accessibility text size, and how do you prevent it?
MediumUse leading and trailing anchors instead of left and right, and let text drive height instead of hard-coding it — those two habits prevent most of the damage.
// Leading and trailing mirror automatically; left and right never do NSLayoutConstraint.activate([ icon.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), label.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 8), label.trailingAnchor.constraint(lessThanOrEqualTo: view.layoutMarginsGuide.trailingAnchor), ]) … - 13
Every button in a horizontal UIStackView came out the same width and the icon got stretched to the full row height — which two properties are wrong?
Easydistributiondivides space along the axis andalignmentpositions views across it: the equal widths come from.fillEqually, the stretched icon from the default alignment.fill.let row = UIStackView(arrangedSubviews: [icon, title, chevron]) row.axis = .horizontal row.spacing = 8 // WRONG: every arranged view ends up the same width, and the icon is stretched // to the row's height because .fill alignment resizes across the axis too. … - 14
The app crashes with "Unable to activate constraint ... because they have no common ancestor" — what did the setup code do in the wrong order?
EasyIt activated a constraint before both views were in the same hierarchy: activating installs the constraint on the nearest common ancestor of the two items, and a view with no superview has no ancestor to install it on.
// WRONG: the anchors are activated while badge is still floating with no superview let badge = UIView() NSLayoutConstraint.activate([ badge.topAnchor.constraint(equalTo: card.topAnchor), // crash: no common ancestor badge.trailingAnchor.constraint(equalTo: card.trailingAnchor), ]) … - 15
Your custom badge view draws a longer number after an update but keeps its old width — what is missing from the view's implementation?
EasyThe view never told Auto Layout that its natural size changed: override
intrinsicContentSize, and callinvalidateIntrinsicContentSize()every time the content that feeds it changes.final class BadgeView: UIView { private let inset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) var count: Int = 0 { didSet { guard count != oldValue else { return } … - 16
A table view's
tableHeaderViewrenders zero points tall no matter what its internal constraints say — how do you size a view to its content by hand?MediumtableHeaderViewis one of the few views UIKit never self-sizes, so you measure it yourself withsystemLayoutSizeFittingand assign the resulting frame.func sizeTableHeader() { guard let header = tableView.tableHeaderView else { return } // Width first: a multi-line label measured at zero width reports one line header.frame.size.width = tableView.bounds.width header.setNeedsLayout() … - 17
A text field at the bottom of a form sits under the keyboard — what is the current UIKit way to lift it, and where did the old notification code go wrong?
MediumConstrain the field to
view.keyboardLayoutGuide.topAnchor: since iOS 15 that guide tracks the keyboard's frame in your view's coordinates and animates with it, so one constraint replaces the whole notification pipeline.// iOS 15+: one constraint, animated in step with the keyboard composer.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ composer.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -8), composer.leadingAnchor.constraint(equalTo: view.leadingAnchor), composer.trailingAnchor.constraint(equalTo: view.trailingAnchor), … - 18
One screen pins the CPU at 100% and freezes while
layoutSubviewsruns over and over — what causes an Auto Layout feedback loop?MediumSomething inside the layout pass invalidates the layout again: you changed constraints from geometry that the same pass just produced, so every pass schedules another one.
// WRONG: the constant is recomputed from the frame this pass produced, // which dirties the layout and schedules the next pass. Forever. override func layoutSubviews() { super.layoutSubviews() insetConstraint.constant = bounds.width * 0.1 layoutIfNeeded() // and this makes it recurse immediately … - 19
The design is two columns on iPad and one on iPhone — do you check the device idiom, the screen width, or something else?
MediumCheck the trait collection's
horizontalSizeClass, because it describes the space your view actually has, while the idiom and the screen describe hardware that may have nothing to do with it.// WRONG: hardware questions, answered before the window even has a size if UIDevice.current.userInterfaceIdiom == .pad { showTwoColumns() } let width = UIScreen.main.bounds.width // deprecated in iOS 16; not your window // RIGHT: the trait describes the space this view actually got private func applyLayout() { … - 20
Two icons pinned to the same leading edge look misaligned because one asset carries a glow around the artwork — what is Auto Layout actually aligning?
MediumAuto Layout aligns alignment rects, not frames — by default the two are identical, but a view can declare that part of its frame is decoration that should not count.
// WRONG: a magic constant that cancels the asset's glow at one call site NSLayoutConstraint.activate([ glowIcon.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: -4), plainIcon.leadingAnchor.constraint(equalTo: container.leadingAnchor), ]) …