Write, review, or improve UIKit code following best practices for view controller lifecycle, Auto Layout, collection views, navigation, animation, memory management, and modern iOS 18–26 APIs. Use when building new UIKit features, refactoring existing views or view controllers, reviewing code quality, adopting modern UIKit patterns (diffable data sources, compositional layout, cell configuration), or bridging UIKit with SwiftUI. Does not cover SwiftUI-only code.
96
100%
Does it follow best practices?
Impact
96%
1.23xAverage score across 9 eval scenarios
Passed
No known issues
Batch constraint activation, priority traps, animation patterns, and iOS 26's .flushUpdates — a comprehensive guide. Auto Layout's internal Cassowary-based solver (NSISEngine) is powerful but unforgiving. Most performance problems stem from a single anti-pattern: constraint churn — destroying and recreating constraints the engine already solved. This guide covers every major optimization surface from the engine internals documented in WWDC 2018 "High Performance Auto Layout" (session 220) through iOS 26's new .flushUpdates animation option announced at WWDC 2025. Every topic includes correct and incorrect Swift code to make the right pattern unmistakable.
NSLayoutConstraint.activate beats individual isActiveEach window owns a single NSISEngine instance — Apple's implementation of the Cassowary incremental simplex solver. The engine is a layout cache and dependency tracker. When you activate a constraint, the engine creates an equation, performs algebraic substitution, and solves for four variables per view (minX, minY, width, height).
When you call NSLayoutConstraint.activate(_:), the internal path routes through +[NSLayoutConstraint _addOrRemoveConstraints:activate:], which wraps all additions inside NSISEngine.withAutomaticOptimizationsDisabled:. This critical method defers intermediate solve passes — the engine adds every equation first, then performs a single solve pass at the end. It also batches the expensive _nearestAncestorLayoutItem and _findCommonAncestorOfItem:andItem: traversals.
Setting isActive = true individually gives the engine no way to know when you're "done." Each activation triggers its own ancestor search and potentially a full solve. The results of intermediate solves between constraint activations are immediately thrown away by the next activation — pure waste.
// ❌ Individual activation — intermediate engine solves between each line
let c1 = view.leadingAnchor.constraint(equalTo: parent.leadingAnchor, constant: 16)
c1.isActive = true // Engine solves → result discarded by next activation
let c2 = view.trailingAnchor.constraint(equalTo: parent.trailingAnchor, constant: -16)
c2.isActive = true // Engine solves again → result discarded
let c3 = view.topAnchor.constraint(equalTo: parent.topAnchor, constant: 8)
c3.isActive = true // Engine solves again → result discarded
let c4 = view.heightAnchor.constraint(equalToConstant: 44)
c4.isActive = true // Final solve — the only one that matters// ✅ Batch activation — single solve pass, deferred optimization
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: parent.leadingAnchor, constant: 16),
view.trailingAnchor.constraint(equalTo: parent.trailingAnchor, constant: -16),
view.topAnchor.constraint(equalTo: parent.topAnchor, constant: 8),
view.heightAnchor.constraint(equalToConstant: 44)
])Apple's documentation confirms: "Typically, using this method is more efficient than activating each constraint individually." The same batching benefit applies to NSLayoutConstraint.deactivate(_:) and to constraint changes made inside updateConstraints(), where the engine treats the entire pass as a batch.
translatesAutoresizingMaskIntoConstraints trapThis property bridges pre–Auto Layout "springs and struts" (autoresizingMask + frame) to the constraint system. When true, UIKit generates NSAutoresizingMaskLayoutConstraint objects that fully specify the view's position and size — creating a fully constrained system before you add a single constraint of your own.
Default values depend on how the view was created:
| Creation method | Default value |
|---|---|
Programmatic (UIView(), UILabel(), etc.) | true |
| Interface Builder (Storyboard / XIB) | false (set automatically) |
UIViewController.view (root view) | true (system-managed) |
Views managed by UITableView/UICollectionView | true |
The most common symptom of forgetting this property is a console flood of Unable to simultaneously satisfy constraints errors containing NSAutoresizingMaskLayoutConstraint entries you never created. The autoresizing mask constraints fully define position and size, so any additional manual constraint creates an over-constrained, unsatisfiable system.
// ❌ Forgot translatesAutoresizingMaskIntoConstraints — ghost constraints
let label = UILabel()
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
// 💥 Console: "Unable to simultaneously satisfy constraints"
// NSAutoresizingMaskLayoutConstraint conflicts with your centering constraints// ✅ Always set to false before adding constraints
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])Additional pitfalls to watch for: views can disappear or collapse to zero size because the autoresizing mask constraints pin them to the default .zero frame. Views can jump to origin (0,0) for the same reason. And a layout that "works" in IB but breaks in code nearly always traces back to IB setting this property to false automatically while code does not.
Do not set translatesAutoresizingMaskIntoConstraints = false on UIViewController.view or on views whose positioning is managed by a parent container (like UITableViewCell.contentView). These views rely on the autoresizing mask bridge.
Ken Ferry (Apple's Auto Layout author) called constraint churn the #1 most common error in client code during WWDC 2018 session 220. Churn means removing and recreating constraints that haven't actually changed — forcing the engine to undo algebraic substitutions and redo them from scratch.
The cost hierarchy inside NSISEngine makes the case clearly:
| Operation | Engine cost |
|---|---|
Modifying .constant | Near-zero — targeted dependency update |
Inequality (≥ / ≤) | Very small (one additional slack variable) |
| Non-required priority | Moderate (Simplex error minimization) |
| Adding / removing constraints | Expensive — full structural modification |
Ken Ferry described .constant modification as "a very, very, very fast one-step update" because the engine's dependency tracker knows exactly which variables are affected and recomputes only those. Adding or removing a constraint restructures the engine's equation system.
// ❌ Constraint churn — tearing down and rebuilding every time
func updateLayout(expanded: Bool) {
NSLayoutConstraint.deactivate(currentConstraints)
currentConstraints.removeAll()
if expanded {
currentConstraints = [/* recreate expanded constraints */]
} else {
currentConstraints = [/* recreate collapsed constraints */]
}
NSLayoutConstraint.activate(currentConstraints)
}// ✅ Create once, toggle or modify
class ExpandableView: UIView {
private var heightConstraint: NSLayoutConstraint!
private var expandedConstraints: [NSLayoutConstraint] = []
private var collapsedConstraints: [NSLayoutConstraint] = []
func setupConstraints() {
// Create ALL constraints once
heightConstraint = heightAnchor.constraint(equalToConstant: 60)
expandedConstraints = [
detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
detailLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12)
]
collapsedConstraints = [
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12)
]
// Activate the initial set
NSLayoutConstraint.activate(collapsedConstraints)
heightConstraint.isActive = true
}
func setExpanded(_ expanded: Bool) {
if expanded {
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
heightConstraint.constant = 120 // Cheap modification
} else {
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
heightConstraint.constant = 60
}
}
}Two additional rules from WWDC 2018: hide views (isHidden = true) instead of removing them from the hierarchy, because removal also removes their constraints from the engine. And separate shared constraints from layout-specific ones — constraints common to all states should be activated once and never touched again.
NSISEngine treats required (1000) and non-required (<1000) constraints as fundamentally different data structures. Required constraints are hard rows in the simplex tableau — they must be satisfied or the system is declared unsatisfiable. Non-required constraints trigger the engine's error minimization system, adding error variables and objective function entries that the Simplex algorithm minimizes.
Switching a constraint between these categories while it's installed would require restructuring the engine's internal representation. Apple chose to forbid it entirely: changing priority from required to non-required (or vice versa) on an installed constraint throws an exception.
*** Assertion failure in -[NSLayoutConstraint setPriority:]
'Mutating a priority from required to not on an installed constraint
(or vice versa) is not supported. You passed priority 250 and the
existing priority was 1000.'The fix is to use 999 instead of 1000 for any constraint whose priority you may need to change at runtime. Priority 999 is virtually required — it will be satisfied before any constraint at 998 or below — but it lives in the non-required category, so you can freely adjust it.
// ❌ Will crash — creating at 1000 then changing to optional
let constraint = view.heightAnchor.constraint(equalToConstant: 200)
constraint.isActive = true // Installed as required (1000)
constraint.priority = UILayoutPriority(750) // 💥 Runtime exception// ✅ Set priority BEFORE activation, use 999 for "virtually required"
let constraint = view.heightAnchor.constraint(equalToConstant: 200)
constraint.priority = UILayoutPriority(999) // Set FIRST
constraint.isActive = true // Then activate
// Now you can freely change priority at runtime:
constraint.priority = UILayoutPriority(250) // Fine — both are non-required
constraint.priority = UILayoutPriority(999) // Also fine// ✅ Practical pattern: two competing constraints at different priorities
let compactHeight = view.heightAnchor.constraint(equalToConstant: 44)
compactHeight.priority = UILayoutPriority(999)
let expandedHeight = view.heightAnchor.constraint(equalToConstant: 200)
expandedHeight.priority = UILayoutPriority(750)
NSLayoutConstraint.activate([compactHeight, expandedHeight])
// Toggle by swapping priorities — no add/remove needed
func setExpanded(_ expanded: Bool) {
compactHeight.priority = expanded ? UILayoutPriority(750) : UILayoutPriority(999)
expandedHeight.priority = expanded ? UILayoutPriority(999) : UILayoutPriority(750)
}Apple's Auto Layout Guide reinforces this: "Avoid giving views required content hugging or compression resistance priorities. It's usually better for a view to be the wrong size than for it to accidentally create a conflict. Use a very high priority (999) instead."
These two priorities control what happens when a view's Auto Layout size disagrees with its intrinsic content size. Content hugging resists the view being made larger than its content (high priority = hugs tightly, won't stretch). Compression resistance resists the view being made smaller than its content (high priority = won't truncate).
Behind the scenes, Auto Layout translates intrinsicContentSize into four implicit NSContentSizeLayoutConstraint instances. For a UILabel with intrinsic size {100, 30}:
width <= 100 at content hugging prioritywidth >= 100 at compression resistance priorityheight <= 30 at content hugging priorityheight >= 30 at compression resistance priorityDefault priorities matter — and differ between Interface Builder and code:
| View | Hugging (code) | Compression Resistance |
|---|---|---|
| UILabel | 250 | 750 |
| UITextField | 250 | 750 |
| UIButton | 250 | 750 |
| UILabel (IB) | 251 | 750 |
That IB difference of 1 point (251 vs 250) is why label+textfield pairs "just work" in Interface Builder with the text field stretching. In code, both default to 250, creating ambiguity.
When two labels share the same content hugging priority and compete for horizontal space, the layout is ambiguous. Auto Layout picks a winner arbitrarily — the result can change between iOS versions.
// ❌ Ambiguous — both labels have same hugging priority (250)
let titleLabel = UILabel()
let valueLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
valueLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(titleLabel)
view.addSubview(valueLabel)
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
valueLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8),
valueLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
valueLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
// ⚠️ Which label stretches? Auto Layout doesn't know — ambiguous layout// ✅ Explicit priorities — titleLabel hugs content, valueLabel stretches
let titleLabel = UILabel()
let valueLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
valueLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(titleLabel)
view.addSubview(valueLabel)
// Extra space → valueLabel grows (lower hugging = willing to stretch)
titleLabel.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal)
valueLabel.setContentHuggingPriority(UILayoutPriority(250), for: .horizontal)
// Not enough space → valueLabel truncates first (lower resistance = yields)
titleLabel.setContentCompressionResistancePriority(UILayoutPriority(751), for: .horizontal)
valueLabel.setContentCompressionResistancePriority(UILayoutPriority(750), for: .horizontal)
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
valueLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8),
valueLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
valueLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])The rule is simple: when there's too much space, the view with the lowest content hugging priority expands. When there's too little space, the view with the lowest compression resistance priority shrinks.
.flushUpdatesThe established pattern for animating constraint changes requires three steps: flush pending layout, change the constraint, then animate a new layout pass. The pre-animation layoutIfNeeded() is essential — without it, any pending constraint changes from earlier in the run loop would also animate, producing unexpected motion.
// ❌ Missing pre-animation flush — unrelated changes animate too
heightConstraint.constant = 300
UIView.animate(withDuration: 0.4) {
self.view.layoutIfNeeded()
}
// If any OTHER constraint changed earlier in the same run loop,
// that change also animates — unintended visual artifacts// ✅ Correct three-step pattern (pre-iOS 26)
view.layoutIfNeeded() // 1. Flush pending changes immediately
heightConstraint.constant = 300 // 2. Make the desired change
UIView.animate(withDuration: 0.4) {
self.view.layoutIfNeeded() // 3. Animate the new layout
}A critical detail: layoutIfNeeded() must be called on the superview (or a common ancestor), not on the constrained view itself. Constraints define relationships between a child and its parent — the parent's subtree must recalculate.
// ❌ Calling layoutIfNeeded on the wrong view
UIView.animate(withDuration: 0.3) {
self.animatedView.layoutIfNeeded() // Won't animate — wrong target
}
// ✅ Call on the parent / common ancestor view
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded() // Parent's subtree recalculates
}UIView.AnimationOptions.flushUpdatesAnnounced at WWDC 2025 in session 243 ("What's New in UIKit"), .flushUpdates eliminates the manual layoutIfNeeded() dance entirely. Available on iOS 26.0+, iPadOS 26.0+, Mac Catalyst 26.0+, tvOS 26.0+, and visionOS 26.0+.
When .flushUpdates is set, UIKit automatically flushes all pending updates (traits, properties, and layout) at key boundaries: before entering the animation scope, before nested animation scopes, before exiting any scope, and before toggling animation enablement within a scope. It implicitly propagates to nested animation scopes.
// ✅ iOS 26+ with .flushUpdates — no manual layoutIfNeeded() needed
UIView.animate(withDuration: 0.4, options: .flushUpdates) {
self.heightConstraint.constant = 300
// UIKit flushes layout automatically — that's it
}// ✅ .flushUpdates with constraint activation/deactivation
UIView.animate(withDuration: 0.3, options: .flushUpdates) {
self.compactConstraints.forEach { $0.isActive = false }
self.expandedConstraints.forEach { $0.isActive = true }
}// ✅ .flushUpdates with UIViewPropertyAnimator
let animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeInOut)
animator.flushUpdates = true
animator.addAnimations {
self.heightConstraint.constant = 300
}
animator.startAnimation()// ✅ Keyboard animation with .flushUpdates (WWDC 2025 example)
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { notification in
let endFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey]
as? CGRect) ?? .zero
let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey]
as? Double) ?? 0.25
UIView.animate(withDuration: duration, options: .flushUpdates) {
self.bottomConstraint.constant = self.view.bounds.maxY - endFrame.minY
}
}iOS 26 also introduces updateProperties(), a new lifecycle method that runs before layoutSubviews() in the top-down pass. Combined with @Observable model tracking, UIKit can now automatically invalidate and rerun layout methods when tracked properties change — no manual setNeedsLayout() calls required.
UIStackView performs no layout itself. It creates and manages Auto Layout constraints on behalf of its arranged subviews. For a .fill distribution, it generates constraints between the top of the first subview and the stack's top edge, between each consecutive pair (with spacing), and between the last subview and the stack's bottom edge. For .equalSpacing or .equalCentering, additional invisible UILayoutGuide objects are inserted between every pair, each with their own constraints.
The constraint explosion problem: each nesting level multiplies constraint count. A simple stack with 5 views might generate ~12 constraints. A nested stack (stack inside a stack) with 5+5 views creates ~30+ constraints. Three levels of nesting can produce 50–100+ constraints per cell. The Cassowary solver's complexity ranges from O(n²) to O(n³) depending on constraint relationships, so more constraints means disproportionately more time.
Worse, when a label's text changes inside a stack view in a cell during scrolling, the intrinsic content size changes, the stack view tears down and rebuilds its constraints, and a full layout pass fires — potentially at 120 fps on ProMotion displays.
// ❌ Deeply nested stacks in a reusable cell — constraint explosion
class BadCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let innerStack1 = UIStackView(arrangedSubviews: [icon, titleLabel])
innerStack1.axis = .horizontal; innerStack1.spacing = 8
let innerStack2 = UIStackView(arrangedSubviews: [subtitleLabel, dateLabel])
innerStack2.axis = .horizontal; innerStack2.spacing = 8
let innerStack3 = UIStackView(arrangedSubviews: [tag1, tag2, tag3])
innerStack3.axis = .horizontal; innerStack3.spacing = 4
let outerStack = UIStackView(arrangedSubviews: [
innerStack1, innerStack2, innerStack3
])
outerStack.axis = .vertical; outerStack.spacing = 4
// 50+ constraints generated internally — recalculated on every reuse
contentView.addSubview(outerStack)
}
}// ✅ Manual constraints in reusable cells — explicit, fast, predictable
class GoodCell: UITableViewCell {
private let icon = UIImageView()
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let dateLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
[icon, titleLabel, subtitleLabel, dateLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview($0)
}
NSLayoutConstraint.activate([
icon.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
icon.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
icon.widthAnchor.constraint(equalToConstant: 40),
icon.heightAnchor.constraint(equalToConstant: 40),
titleLabel.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 12),
titleLabel.topAnchor.constraint(equalTo: icon.topAnchor),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
subtitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -12),
dateLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
dateLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
])
// ~14 constraints total — stable, no churn on reuse
}
required init?(coder: NSCoder) { fatalError() }
}Best practices for cells: limit stack view nesting to 1–2 levels maximum. Set up the stack structure once in init and only update content (text, images) in configure. For complex cell layouts, prefer manual constraints or layout libraries like PinLayout/FlexLayout. If using stack views, avoid toggling isHidden on arranged subviews during rapid scrolling — each toggle triggers a full constraint rebuild inside the stack.
Create a Symbolic Breakpoint in Xcode (Breakpoint Navigator → + → Symbolic Breakpoint) with the symbol UIViewAlertForUnsatisfiableConstraints. This fires at the exact moment a constraint conflict is detected, before the engine breaks a constraint to recover. You get full debugger access to inspect all views and constraints involved.
// In LLDB when the breakpoint fires:
(lldb) po $arg1 // Print all conflicting constraints// ❌ Unnamed constraints — cryptic memory addresses in logs
let c = view.widthAnchor.constraint(equalToConstant: 200)
c.isActive = true
// Console: <NSLayoutConstraint:0x6000025a1e00 UIView:0x7f88...width == 200>// ✅ Named constraints — immediately identifiable in logs
let c = view.widthAnchor.constraint(equalToConstant: 200)
c.identifier = "ProfileCard.width"
c.isActive = true
// Console: <NSLayoutConstraint:0x6000025a1e00 'ProfileCard.width' UIView:0x7f88...width == 200>_autolayoutTrace reveals ambiguity across the entire view treeThis private API prints the view hierarchy with * markers on ambiguous views:
// In LLDB (Swift projects require the Obj-C bridge):
(lldb) expr -l objc++ -O -- [[UIWindow keyWindow] _autolayoutTrace]
// Output:
*<UIView:0x7f8b9c4...> ← * means AMBIGUOUS layout
| <UILabel:0x7f8b9c3...>
| *<UILabel:0x7f8b9c2...> ← also ambiguousAdd this alias to ~/.lldbinit for quick access:
command alias alt expr -l objc++ -O -- [[UIWindow keyWindow] _autolayoutTrace]constraintsAffectingLayout(for:) returns every constraint affecting a specific view on a given axis — invaluable for understanding why a view ended up at an unexpected size:
// In LLDB:
(lldb) po myView.constraintsAffectingLayout(for: .horizontal)
(lldb) po myView.constraintsAffectingLayout(for: .vertical)hasAmbiguousLayout and exerciseAmbiguityInLayout() let you detect and visualize ambiguity:
#if DEBUG
for subview in view.subviews {
if subview.hasAmbiguousLayout {
print("⚠️ Ambiguous: \(subview)")
subview.exerciseAmbiguityInLayout() // Toggles between valid solutions
}
}
#endifUse Xcode's Debug View Hierarchy (the layered-rectangles button in the debug bar) to visually inspect constraints — purple lines indicate satisfied constraints, and warnings appear for conflicts or ambiguity. The third-party tool wtfautolayout.com parses console constraint errors into visual diagrams.
updateConstraints() demands idempotencyApple's guidance is unambiguous: "Do not deactivate all your constraints, then reactivate the ones you need. Instead, your app must have some way of tracking your constraints, and validating them during each update pass. Only change items that need to be changed."
The method runs from leaf views toward the window during every render loop iteration — potentially 120 times per second on ProMotion displays. Removing and recreating constraints inside it is the textbook definition of constraint churn.
// ❌ The #1 anti-pattern — constraint churn at up to 120 fps
override func updateConstraints() {
NSLayoutConstraint.deactivate(allConstraints)
allConstraints.removeAll()
// Recreate everything from scratch...
allConstraints.append(widthAnchor.constraint(equalToConstant: currentWidth))
allConstraints.append(heightAnchor.constraint(equalToConstant: currentHeight))
NSLayoutConstraint.activate(allConstraints)
super.updateConstraints()
}// ✅ Idempotent — only changes what actually needs changing
override func updateConstraints() {
if isExpanded {
collapsedConstraints.forEach { $0.isActive = false }
expandedConstraints.forEach { $0.isActive = true }
} else {
expandedConstraints.forEach { $0.isActive = false }
collapsedConstraints.forEach { $0.isActive = true }
}
super.updateConstraints() // MUST be last
}// ✅ Best practice — skip updateConstraints entirely, set up in init/viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// Create all constraints once
heightConstraint = box.heightAnchor.constraint(equalToConstant: 100)
NSLayoutConstraint.activate([
box.centerXAnchor.constraint(equalTo: view.centerXAnchor),
box.centerYAnchor.constraint(equalTo: view.centerYAnchor),
box.widthAnchor.constraint(equalToConstant: 200),
heightConstraint
])
}
// Respond to state changes directly — no updateConstraints needed
func toggleExpanded() {
heightConstraint.constant = isExpanded ? 300 : 100
}Apple recommends: "It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred." Reserve updateConstraints() only for cases where you need to coalesce many rapid property changes into a single constraint pass for performance. Always call super.updateConstraints() last, and never call setNeedsUpdateConstraints() or setNeedsLayout() inside it — both create feedback loops.
Presented by Ken Ferry and Kasia Wawer, this remains the definitive session on Auto Layout internals. No dedicated Auto Layout performance session has been given since. The critical insights:
The engine is a Cassowary-based incremental simplex solver that functions as a layout cache and dependency tracker. Each window owns one engine instance. Adding a constraint creates an equation; the engine solves through algebraic substitution. Non-required priorities invoke the Simplex algorithm (explicitly named) for error minimization. Ken Ferry noted: "This is the Simplex algorithm... super old, developed during World War II. Before computers."
Performance scales linearly for independent view hierarchies — unrelated views create separate equation blocks with no overlapping variables. Cross-hierarchy constraints tie blocks together and increase cost. The session demonstrated that iOS 12 rewrote UICollectionView's internal Auto Layout usage, taking self-sizing cells from "janky and bad" to full frame rate with zero app code changes.
The session previewed a new Instruments tool for layout profiling that tracks constraint churn by view count, constraint remove/change instances, and text sizing time.
Session 10118 ("What's New in UIKit") and session 10145 ("Enhance your UI animations and transitions") introduced SwiftUI animation types for UIKit views: UIView.animate(.spring(duration: 0.5)) { ... }. Also introduced: automatic trait tracking in layoutSubviews and drawRect (UIKit records which traits you access and automatically invalidates when they change), UIUpdateLink (a better CADisplayLink), and unified gesture recognition across UIKit and SwiftUI.
.flushUpdates and updateProperties()Session 243 ("What's New in UIKit") introduced the two most significant layout changes since iOS 12. .flushUpdates (detailed in section 6) eliminates manual layoutIfNeeded() calls for constraint animation. updateProperties() adds a new lifecycle phase that runs before layoutSubviews(), separating property application from layout calculation. Combined with automatic @Observable tracking across layoutSubviews(), viewWillLayoutSubviews(), updateProperties(), and cell configuration handlers, UIKit can now reactively invalidate only the methods that depend on changed model properties. This observation tracking is back-deployable to iOS 18 via the UIObservationTrackingEnabled Info.plist key.
The mental model Ken Ferry offered in 2018 remains the foundation: the engine is a layout cache and dependency tracker that performs basic algebra. You only pay for what you use, independent hierarchies scale linearly, and .constant modifications are near-free because the engine tracks dependencies precisely.
Three patterns matter above all others. First, create constraints once and modify them — never churn. Second, use 999 instead of 1000 for any constraint whose priority you might change. Third, batch everything — NSLayoutConstraint.activate(_:), updateConstraints(), or iOS 26's .flushUpdates all tell the engine to defer intermediate solves.
.flushUpdates is the first major quality-of-life improvement to constraint animation in nearly a decade. It doesn't change what the engine does — it changes what you have to remember to do. Combined with @Observable tracking and updateProperties(), UIKit's layout system in 2025–2026 is moving toward a reactive model where the framework handles invalidation automatically, and developers focus on declaring intent rather than managing engine passes.translatesAutoresizingMaskIntoConstraints = false set on every programmatically created view before constrainingNSLayoutConstraint.activate([]), not individual .isActive = trueisActive toggle or .constant modification instead.required (1000) at runtime — using 999 for mutable prioritiesconstraint.identifier = "MyView.height")updateConstraints() is idempotent — no remove-all-then-recreate; super.updateConstraints() called lastlayoutIfNeeded() on superviewsetNeedsLayout() or setNeedsUpdateConstraints() inside layoutSubviews or viewDidLayoutSubviews.flushUpdates used for constraint animation where available