DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Layout System Masterclass

SwiftUI layout feels magical — until it doesn’t.

That’s when you start seeing:

  • views ignoring your frame
  • GeometryReader breaking everything
  • stacks not aligning as expected
  • text truncating randomly
  • layouts behaving differently on iPad
  • “why is this view zero height?”

The reason is simple:

SwiftUI has a layout system — and most developers never learn how it actually works.

This post breaks down SwiftUI’s layout engine from first principles, using the modern APIs.

Once you understand this, layout bugs stop being mysterious.


🧠 The Core Rule of SwiftUI Layout

SwiftUI layout happens in three phases:

  1. Parent proposes a size
  2. Child chooses its size
  3. Parent positions the child

That’s it.

Everything — VStack, GeometryReader, frames, alignment — builds on this.


📐 1. Proposed Size vs Actual Size

A parent does not tell a child what size it must be.

It proposes a size.

The child can:

  • accept it
  • shrink
  • grow
  • ignore it

Example:

Text("Hello")
    .frame(width: 300)
Enter fullscreen mode Exit fullscreen mode

The frame proposes 300pt width.

The text may:

  • use less
  • use more (if allowed)
  • clip
  • truncate

Understanding this explains 80% of layout behavior.


🧱 2. How Stacks Actually Lay Out Views

VStack / HStack behavior:

  • propose unlimited space on the stack axis
  • propose constrained space on the cross axis
  • ask children for their ideal sizes
  • distribute remaining space

Example:

VStack {
    Text("A")
    Text("B")
}
Enter fullscreen mode Exit fullscreen mode

Each child gets:

  • unlimited height
  • constrained width

This is why vertical content often grows naturally.


⚖️ 3. Layout Priority (Why Some Views Get More Space)

When space is limited, SwiftUI uses layoutPriority.

Text("Important")
    .layoutPriority(1)

Text("Less important")
Enter fullscreen mode Exit fullscreen mode

Higher priority:

  • resists compression
  • gets space first

Use this for:

  • titles
  • primary content
  • buttons that must stay visible

📏 4. The Truth About .frame()

.frame() does not size a view.

It:

  • wraps the view
  • proposes a new size
  • optionally clips or aligns
Text("Hello")
    .frame(maxWidth: .infinity)
Enter fullscreen mode Exit fullscreen mode

This tells the parent:

“I am willing to be as wide as possible.”

It does not force width unless constrained by a parent.


⚠️ 5. GeometryReader: Powerful but Dangerous

GeometryReader:

  • always expands to fill all available space
  • changes the layout behavior of its parent
  • breaks intrinsic sizing

Example problem:

VStack {
    GeometryReader { geo in
        Text("Hello")
    }
}
Enter fullscreen mode Exit fullscreen mode

The VStack now stretches.

Rule:
📌 Use GeometryReader only when you truly need size info.

Better alternatives:

  • Layout protocol
  • ViewThatFits
  • preference keys

🔄 6. Alignment Guides (Underrated but Powerful)

Alignment guides let children control alignment.

HStack(alignment: .top) {
    Text("Title")
        .alignmentGuide(.top) { _ in 20 }

    Text("Body")
}
Enter fullscreen mode Exit fullscreen mode

Use cases:

  • baseline alignment
  • optical alignment
  • custom grids
  • complex text layouts

🧩 7. The New Layout Protocol

For complex layouts, stop abusing stacks.

Use Layout:

struct GridLayout: Layout {
    func sizeThatFits(
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) -> CGSize {
        CGSize(width: 300, height: 200)
    }

    func placeSubviews(
        in bounds: CGRect,
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) {
        for (index, subview) in subviews.enumerated() {
            subview.place(
                at: CGPoint(x: bounds.minX + 50, y: bounds.minY + CGFloat(index) * 50),
                proposal: .unspecified
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This gives you:

  • full control
  • predictable behavior
  • performance
  • no layout hacks

📱 8. Adaptive Layout (iPhone vs iPad vs Mac)

SwiftUI layouts must adapt.

Tools:
horizontalSizeClass

  • ViewThatFits
  • AnyLayout

Example:

ViewThatFits {
    HStack { content }
    VStack { content }
}
Enter fullscreen mode Exit fullscreen mode

SwiftUI picks the first layout that fits.

This is cleaner than if/else layout logic.


🚀 9. Performance Rules for Layout

✔ Avoid deep stack nesting
✔ Avoid GeometryReader in lists
✔ Use Layout for complex arrangements
✔ Minimize view invalidations
✔ Prefer intrinsic sizing
✔ Measure once, reuse results

Layout is part of rendering — poor layout = poor performance.


🧠 10. Mental Model Cheat Sheet

If layout behaves oddly, ask:

  1. What size is being proposed?
  2. Is the child allowed to choose?
  3. Is a frame wrapping the view?
  4. Is GeometryReader expanding things?
  5. Are priorities fighting?
  6. Is the parent constrained?

Answering these solves almost every bug.


🚀 Final Thoughts

SwiftUI layout is not random.

It’s:

  • deterministic
  • compositional
  • powerful

Once you understand:

  • proposal → response → placement

You stop fighting SwiftUI — and start designing with it.

Top comments (0)