Why I Choose Manual Frame Layout Over Auto Layout
Every UIKit tutorial starts with Auto Layout. Drag constraints in Interface Builder, pin edges, set priorities. It works — until it doesn't. After years of fighting ambiguous layouts, breaking constraints during animations, and debugging unsatisfiable constraint logs, I switched to manual frame-based layout for Perdiem. I haven't looked back.
The Case Against Auto Layout
Auto Layout solves a genuine problem: describing relationships between views that adapt to different screen sizes. But it comes with real costs.
Constraint debugging is painful. When constraints conflict, UIKit dumps a wall of text into the console that's almost impossible to parse. You end up binary-searching through your constraint setup to find the offender.
Animations fight the system. Animating constraints means calling layoutIfNeeded() inside an animation block and hoping the constraint solver produces the right intermediate frames. For complex, interruptible animations — like the ones in Perdiem's navigation transitions — this is unreliable.
Performance matters at scale. The constraint solver runs on the main thread. For simple layouts it's imperceptible, but screens with dozens of views and complex relationships can introduce measurable layout time.
How Manual Layout Works in Practice
Every custom view in Perdiem overrides layoutSubviews(). The pattern is consistent:
override func layoutSubviews() {
super.layoutSubviews()
let size: CGSize = self.bounds.size
self.titleLabel.zeroedContextualFrame = {
let labelSize: CGSize = self.titleLabel.sizeThatFits(CGSize(
width: size.width - 40,
height: CGFloat.greatestFiniteMagnitude
))
var frame = CGRect()
frame.size.width = labelSize.width
frame.origin.x = 20
frame.size.height = labelSize.height
frame.origin.y = 16
return frame
}()
self.subtitleLabel.zeroedContextualFrame = {
var frame = CGRect()
frame.size.width = size.width - 40
frame.origin.x = 20
frame.size.height = 20
frame.origin.y = self.titleLabel.contextualFrame.maxY + 8
return frame
}()
}
Each view's frame is computed as a closure, keeping intermediate variables locally scoped. Views reference each other's positions directly — self.titleLabel.contextualFrame.maxY + 8 is clearer than a constraint chain.
The contextualFrame Pattern
One subtlety: UIKit's .frame property is affected by the view's .transform. If you're scaling or rotating a view, setting .frame produces unexpected results. Perdiem uses a helper called contextualFrame that sets bounds and center independently, bypassing transform interference:
extension UIView {
var contextualFrame: CGRect {
get { /* computed from bounds + center */ }
set {
self.bounds.size = newValue.size
self.center = CGPoint(
x: newValue.midX,
y: newValue.midY
)
}
}
}
This is critical for animated views. During a scale transition, you can still lay out child views correctly because their frames aren't distorted by the parent's transform.
When This Approach Wins
Animations become trivial. You control every pixel. Want a view to slide from the right while growing taller? Just animate the frame values directly. No constraint priorities, no layoutIfNeeded() timing issues.
Debugging is straightforward. A view is in the wrong place? Print its frame. The calculation is right there in layoutSubviews(), not spread across a graph of constraint relationships.
Performance is predictable. Frame calculations are simple arithmetic — addition, subtraction, multiplication. No solver, no ambiguity resolution, no priority ordering.
When Auto Layout Is Still the Right Choice
I'm not dogmatic about this. Auto Layout is great for:
- Interface Builder-driven screens where visual editing matters
- Accessibility and Dynamic Type where system-driven layout adaptation is valuable
- Stack views and simple form layouts where constraints are genuinely simpler
But for a custom, animation-heavy app like Perdiem, manual layout gives me the control I need. Every transition is smooth, every layout is predictable, and I spend zero time debugging constraint conflicts.