I need to talk about something that's been bothering me for twelve years.
In 2012, Apple gave us Auto Layout. In 2014, they gave us size classes. In 2019, they gave us SwiftUI. Each iteration promised the same thing: you'll never have to think about frames again.
And every single time, I end up in layoutSubviews, manually calculating a CGRect, because the "layout system" decided my button should be 0.3333 points off-center and I refuse to live like that.
"Auto Layout is a constraint satisfaction system. You know what else is a constraint satisfaction system? Prison. Both involve rules you didn't write, outcomes you can't predict, and occasionally you just end up on the floor."
The Thesis
Here it is. My position. The hill I will die on, and I've been dying on it slowly since iOS 5:
Every layout problem in the history of UIKit is, at its core, a CGRect calculation. Auto Layout doesn't eliminate the calculation — it hides it behind a constraint solver that sometimes disagrees with you about what "centered" means. SwiftUI doesn't eliminate it either — it just wraps it in a layout protocol that calls sizeThatFits under the hood, which is just a fancy way of saying "give me a CGRect."
You are always computing rectangles. The question is whether you do it yourself, like a professional, or you delegate it to a system that may or may not respect your intentions.
I choose to do it myself.
A CGRect is four numbers: x, y, width, height. If you can do arithmetic on four numbers, you can lay out any screen. Auto Layout introduced a way to describe those four numbers using 47 different constraint types, 6 priority levels, and a partridge in a pear tree. The frame was right there the whole time.
The Framework: RectKit
I've been working on this for three years. It's called RectKit, and it replaces Auto Layout, SwiftUI's layout system, and arguably the need for Interface Builder, with a single concept: everything is a function from a parent rect to child rects.
protocol RectCalculable { func calculateRect(in bounds: CGRect) -> CGRect } extension UIView: RectCalculable { func calculateRect(in bounds: CGRect) -> CGRect { // Override this. This is where the truth lives. return bounds } }
That's it. That's the architecture. Every view knows how to calculate its own rectangle given a parent rectangle. The parent calls the child. The child returns a rect. No constraints. No ambiguous layouts. No "Unable to simultaneously satisfy constraints" followed by a stack trace that reads like a ransom note.
Real-World Example: A Profile Screen
Let's build a profile screen. In Auto Layout, this is 40-60 lines of constraint code, a scroll view that may or may not actually scroll (the odds are 50/50 and you know it), and at least one contentLayoutGuide that you'll set up wrong the first time.
In RectKit:
override func layoutSubviews() { super.layoutSubviews() let b = bounds.inset(by: safeAreaInsets) // Avatar: 80x80, centered horizontally, 16pt from top avatarView.frame = CGRect( x: b.midX - 40, y: b.minY + 16, width: 80, height: 80 ) // Name label: full width, 12pt below avatar let nameSize = nameLabel.sizeThatFits(CGSize(width: b.width - 32, height: .greatestFiniteMagnitude)) nameLabel.frame = CGRect( x: b.minX + 16, y: avatarView.frame.maxY + 12, width: b.width - 32, height: nameSize.height ) // Bio: below name, same margins let bioSize = bioLabel.sizeThatFits(CGSize(width: b.width - 32, height: .greatestFiniteMagnitude)) bioLabel.frame = CGRect( x: b.minX + 16, y: nameLabel.frame.maxY + 8, width: b.width - 32, height: bioSize.height ) // Stats row: three equal columns below bio let statsY = bioLabel.frame.maxY + 20 let statsW = (b.width - 32) / 3 for (i, stat) in statViews.enumerated() { stat.frame = CGRect( x: b.minX + 16 + statsW * CGFloat(i), y: statsY, width: statsW, height: 60 ) } }
Forty lines. Every view's position is derived from the view above it. You can read it top-to-bottom and know exactly where everything is. No constraint inspector. No visual debugger. No praying.
The equivalent Auto Layout code would involve anchoring the avatar's centerXAnchor to the view's centerXAnchor, its topAnchor to the safeAreaLayoutGuide.topAnchor with a constant of 16, its widthAnchor and heightAnchor to 80 — and then doing that for every single view, hoping none of them conflict with each other, and eventually running the app to discover that the stats row is off-screen because you forgot to set translatesAutoresizingMaskIntoConstraints to false on one of the stat views.
Don't tell me you've never shipped that bug. I know you have. Everyone has. It's the iOS developer's original sin.
The CGRectDivide Renaissance
Everyone forgot about CGRectDivide. Apple deprecated the C function in Swift, and the whole community just… moved on. Like it never happened. Like we didn't have a perfectly good function for slicing rectangles into smaller rectangles.
I brought it back.
extension CGRect { func sliceFrom(_ edge: CGRectEdge, amount: CGFloat) -> (slice: CGRect, remainder: CGRect) { var slice: CGRect = .zero var remainder: CGRect = .zero __CGRectDivide(self, &slice, &remainder, amount, edge) return (slice, remainder) } } // Build an entire screen by slicing one rect var remaining = bounds.inset(by: safeAreaInsets) let navBar: CGRect (navBar, remaining) = remaining.sliceFrom(.minYEdge, amount: 44) let footer: CGRect (footer, remaining) = remaining.sliceFrom(.maxYEdge, amount: 83) let sidebar: CGRect (sidebar, remaining) = remaining.sliceFrom(.minXEdge, amount: 260) // 'remaining' is now the content area. Perfectly carved. contentView.frame = remaining
You start with one rectangle — the screen — and you slice it. Top slice: navigation bar. Bottom slice: footer. Left slice: sidebar. What's left is your content area. It's like cutting a pizza, except the pizza is deterministic and nobody argues about the slices.
I showed this to a junior developer and she said, "This looks like a Mondrian painting." I said, "Mondrian never had an ambiguous layout." She didn't laugh. I think she was too young to know who Mondrian is. I am old.
But What About Dynamic Type?
This is the argument. The trump card. The thing Auto Layout advocates throw at me like a holy water grenade:
"But what about Dynamic Type? What about accessibility? What about text that might be 14pt or might be 40pt depending on user settings?"
Here's what I do: I call sizeThatFits and I use the result. That's it. That's the Dynamic Type strategy.
let labelSize = titleLabel.sizeThatFits(CGSize( width: bounds.width - 32, height: CGFloat.greatestFiniteMagnitude )) titleLabel.frame = CGRect( x: 16, y: previousView.frame.maxY + 8, width: bounds.width - 32, height: labelSize.height )
The label tells me how tall it needs to be. I give it that height. If the user has set their text to 40pt, the label will be taller. The views below it will shift down. Everything flows. No constraints needed. Just arithmetic.
Is it more code than a constraint? Yes. Do I know exactly what will happen? Also yes. Have you ever had an Auto Layout constraint animate to the wrong position during a Dynamic Type change and watched your labels overlap like a ransom note? I have. I was in a code review. Someone's label was rendering on top of someone else's label and together they spelled something inappropriate. We shipped it. It was in production for three days.
The RectKit Layout Pass
Here's where I admit this got slightly out of hand.
RectKit has its own layout engine. Not a constraint solver — I need to be clear about that. It's a rect propagation engine. Parent computes its rect. Parent asks each child to compute its rect given the remaining space. Children respond. Parent moves to the next child.
class RectLayout { enum Direction { case vertical, horizontal } let direction: Direction let spacing: CGFloat let padding: UIEdgeInsets private(set) var items: [RectLayoutItem] = [] func compute(in bounds: CGRect) -> [CGRect] { var remaining = bounds.inset(by: padding) var rects: [CGRect] = [] for (i, item) in items.enumerated() { let size = item.measure(in: remaining) let edge: CGRectEdge = direction == .vertical ? .minYEdge : .minXEdge var slice: CGRect (slice, remaining) = remaining.sliceFrom(edge, amount: direction == .vertical ? size.height : size.width) rects.append(slice) // Spacing (except after last item) if i < items.count - 1 { (_, remaining) = remaining.sliceFrom(edge, amount: spacing) } } return rects } }
Yes, I've reinvented stack views. I know. The difference is that my version doesn't sometimes randomly set a view's height to zero for reasons that can only be understood by reading WebKit source code. My version just does math. Predictable, boring, honest math.
I once spent a full workday debugging a UIStackView that was collapsing its arranged subviews on iOS 14 but not iOS 15. The root cause turned out to be an interaction between isHidden, alpha, and the stack view's internal _hiddenForReuse property, which is private API, which means I couldn't even read the documentation for the thing that was breaking my layout because the documentation doesn't exist. My RectLayout has never done this. It can't do this. It doesn't know how. It's too simple. Simplicity is a feature.
The Rotation Problem (Solved)
Everyone thinks manual layout falls apart during rotation. It doesn't. You know what falls apart during rotation? Your constraint priorities. Your compressed hugging. Your content resistance. All those abstract concepts that sound like a therapy session.
My rotation handler:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { _ in self.view.setNeedsLayout() self.view.layoutIfNeeded() }) }
Three lines. The views recalculate their rects with the new bounds. They animate to the new positions. It works in portrait. It works in landscape. It works on iPad multitasking because — and this is the key insight — the math doesn't care what the screen size is. b.midX is b.midX whether the screen is 375 points wide or 1024 points wide. The rect adjusts. The views follow. Nobody files a radar.
What About SwiftUI?
SwiftUI uses a layout protocol. Let me show you what that layout protocol does under the hood:
struct MyLayout: Layout { func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { // return a size (a rect without position) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { // place each subview in... a CGRect } }
It's rect calculation. It was always rect calculation. Apple wrapped it in a protocol with a ProposedViewSize that's basically a CGSize with extra steps, and a placeSubviews method that takes — and I want you to really look at this — a CGRect called bounds.
They're not even hiding it anymore. SwiftUI's layout system is just rect calculation with better marketing. I've been writing the same code since iOS 3. I was just ahead of the curve.
Performance
Auto Layout runs a Cassowary constraint solver. It's a simplex-based algorithm adapted from academic research. It's O(n) for simple layouts and O(n²) or worse for complex ones with priority conflicts. There was a famous WWDC session about this. They showed a graph. The graph went up and to the right, which is bad when the Y axis is "time."
My layout pass: iterate views, do arithmetic, assign frames. O(n). Always. There is no solver. There is no "ambiguous layout" state because there is no system trying to figure out what I meant. I said what I meant. I wrote the numbers down.
On a screen with 50 views, my layout pass takes 0.3ms. Auto Layout takes 2-4ms. You won't notice on a modern iPhone. You will absolutely notice if you're building a chat app with hundreds of cells and each cell has 15 subviews and the constraint solver runs during scrolling and your frame rate drops to 54fps and your PM asks "why does it feel janky?" and you say "the Cassowary solver is struggling" and they look at you like you just named a bird.
The Converts
People think I'm alone in this. I'm not. There is a community. We don't have a Slack — we have a mailing list, because we're the kind of people who still use mailing lists. We call ourselves the Frame Gang, which sounds like a 90s rap group and I'm fine with that.
Members include:
- A developer at a major social media company who told me their entire feed is manually laid out for performance reasons. The feed you scroll through every day. Frames. All frames. The biggest app in the world runs on CGRect.
- An indie developer who ships a weather app that supports every screen size since the iPhone 5 and has never used a single constraint. "I just do the math," she told me. Hero.
- A former Apple engineer who asked not to be named but who told me, verbatim: "We built Auto Layout so developers wouldn't have to do frame math. We still do frame math internally." I cried a little.
The Manifesto
I'm going to end with this. It's what I tell every developer who joins my team:
- The frame is truth. A view's
frameis where it is. Not where constraints say it should be. Not where the layout guide suggests. Where it is. - Arithmetic doesn't have bugs.
x + 16is alwaysx + 16. A leading constraint with a constant of 16 relative to the safe area layout guide's leading anchor is usually that, unless something else has a higher priority, or the safe area changes, or the device rotates and the constraint temporarily resolves to something else during the transition. - Readability is a layout tool. I can read my
layoutSubviewstop to bottom and know exactly where every view goes. I cannot read 40 constraints in a storyboard and know anything. Nobody can. We pretend we can. We're lying. - You are always calculating rects. Auto Layout calculates rects. SwiftUI calculates rects. The GPU renders rects. CoreAnimation composites rects. The question is never "should I calculate rects?" The question is "should I let a solver calculate them for me and hope it agrees with what I wanted?"
I choose to calculate them myself.
I choose the frame.
I choose to Believe™.
RectKit is not open source because last time I open-sourced something, 400 people filed issues in a language I don't speak and someone forked it, added Auto Layout support, and called it "RectKit Pro." It is available upon request. Email me. Or don't. The math is in this post. You already have everything you need.