Layout: Stacks, Frames & GeometryReader
Reviewed & published by Brayan K
SwiftUI lays out screens with simple, composable containers. You'll arrange views with VStack, HStack, and ZStack, control gaps and alignment, size views with .frame, and reach for GeometryReader when you need to lay out relative to the available space.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Building iOS Apps with SwiftUI • Intermediate
What You'll Learn in This Lesson
- • Stack views with VStack / HStack / ZStack
- • Control gaps with spacing and lines with alignment
- • Push views apart with Spacer
- • Size views with .frame
- • Lay out relative to space with GeometryReader
1️⃣ Stacks — VStack, HStack, ZStack
The three containers cover every direction: VStack goes top-to-bottom, HStack goes left-to-right, and ZStack layers views back-to-front so they overlap. Nest them freely to build any arrangement.
import SwiftUI
// The three core containers:
// VStack -> vertical (top to bottom)
// HStack -> horizontal (left to right)
// ZStack -> depth (back to front, overlapping)
struct StacksDemo: View {
var body: some View {
VStack { // stacked vertically
Text("Title")
HStack { // a row inside the column
Image(systemName: "star")
Text("Rated 5 stars")
}
ZStack { // overlapping layers
Color.blue // background layer
Text("On top") // front layer
}
}
}
}2️⃣ Spacing, Alignment & Spacer
spacing sets the gap between children; alignment lines them up across the other axis. A Spacer expands to fill free space, which is the simplest way to push views to opposite ends.
import SwiftUI
// 'spacing' sets the gap between children. 'alignment' sets how they line up
// across the OTHER axis (for a VStack that's the horizontal axis).
struct SpacedColumn: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Left-aligned heading")
Text("Each line sits 12 points below the last")
Text("and all start at the leading edge")
}
}
}
// A Spacer expands to push views apart, filling the free space.
struct ToolbarRow: View {
var body: some View {
HStack {
Text("Left")
Spacer() // pushes the two ends apart
Text("Right")
}
}
}3️⃣ Sizing with .frame
Use .frame to propose a size. Fixed numbers pin exact dimensions; maxWidth: .infinity lets a view grow to fill the available width — handy for full-width buttons and rows.
import SwiftUI
// .frame proposes a SIZE for a view. Use fixed numbers, or maxWidth/maxHeight
// with .infinity to grow into the available space.
struct FrameDemo: View {
var body: some View {
VStack(spacing: 16) {
Text("Fixed box")
.frame(width: 120, height: 44)
.background(Color.gray.opacity(0.3))
Text("Full width")
.frame(maxWidth: .infinity) // grow horizontally
.background(Color.green.opacity(0.3))
}
}
}4️⃣ GeometryReader — Layout Relative to Space
When you need a view sized relative to what's available — say, half the parent's width — wrap it in a GeometryReader. It hands you a geometry proxy whose size tells you the proposed width and height. Use it sparingly: it claims all the space offered to it.
import SwiftUI
// GeometryReader hands you a proxy describing the space proposed to it,
// so you can size children RELATIVE to what's available.
struct HalfWidthBar: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.purple)
// half the available width, fixed 20pt tall
.frame(width: geometry.size.width / 2, height: 20)
}
.frame(height: 20)
}
}Your turn. Fill in the blanks to build a row with an icon, a left-aligned two-line label, and a trailing spacer.
// 🎯 YOUR TURN — fill in each ___ then preview in Xcode.
import SwiftUI
struct CardRow: View {
var body: some View {
// 1) Lay an icon and two lines of text out as a ROW
___ { // 👉 the horizontal container
Image(systemName: "bolt.fill")
// 2) Stack the two text lines VERTICALLY, left-aligned
___(alignment: .leading) { // 👉 the vertical container
Text("Power")
Text("Fully charged")
}
// 3) Push everything to the left edge
___() // 👉 the flexible view that fills space
}
.padding()
}
}Pro Tips
- 💡 Reach for stacks first. Most layouts are just nested VStack/HStack with a Spacer.
- 💡 Use maxWidth: .infinity instead of guessing pixel widths for full-width content.
- 💡 GeometryReader sparingly. It takes all offered space, so it can surprise your layout — wrap it tightly.
Common Errors (and the fix)
- Everything centered when you wanted left-aligned — set alignment: .leading on the VStack.
- Button only as wide as its text — add .frame(maxWidth: .infinity) to grow it.
- GeometryReader pushes content to the top-left — its content aligns top-leading by default; wrap or align as needed.
Mini-Challenge: Profile Header
Build a header where a circular avatar (an SF Symbol) sits on the left and, to its right, a left-aligned VStack shows a name in a large bold font above a handle. Put a Spacer after the text so the whole row stretches full width, and give the row some .padding(). Bonus: use a GeometryReader to make a progress bar exactly 80% of the available width.
🎉 Lesson Complete!
- ✅ VStack / HStack / ZStack arrange views vertically, horizontally, and in layers
- ✅ spacing sets gaps; alignment lines children up
- ✅ Spacer pushes views apart; .frame sizes them
- ✅ GeometryReader lets you size relative to available space
Practice quiz
What does a VStack do?
- Arranges children top to bottom (vertically)
- Arranges children left to right
- Overlaps children
- Hides children
Answer: Arranges children top to bottom (vertically). VStack stacks its child views vertically, one above the next.
What does an HStack do?
- Arranges children vertically
- Arranges children horizontally (left to right)
- Stacks them back to front
- Scrolls children
Answer: Arranges children horizontally (left to right). HStack arranges its child views horizontally in a row.
What does a ZStack do?
- Arranges children in a row
- Layers children back-to-front, overlapping them
- Sorts children by size
- Spaces children evenly
Answer: Layers children back-to-front, overlapping them. ZStack overlays its children on top of one another along the z-axis.
How do you set the gap between items in a VStack?
- VStack(gap: 8)
- VStack(spacing: 8)
- VStack(padding: 8)
- VStack(margin: 8)
Answer: VStack(spacing: 8). Pass the spacing parameter, e.g. VStack(spacing: 8), to control the gap between children.
What does the .frame(width:height:) modifier do?
- Adds a border
- Proposes a size for a view
- Changes the font
- Hides the view
Answer: Proposes a size for a view. .frame proposes a specific size (or sizing constraints) for the view it modifies.
How do you make a view expand to fill available width?
- .fill()
- .expand()
- .frame(maxWidth: .infinity)
- .stretch()
Answer: .frame(maxWidth: .infinity). .frame(maxWidth: .infinity) lets the view grow to take all available horizontal space.
What does the alignment parameter of a VStack control?
- The spacing
- How children line up along the horizontal axis (leading/center/trailing)
- The background color
- The animation speed
Answer: How children line up along the horizontal axis (leading/center/trailing). In a VStack, alignment sets the horizontal alignment of children, e.g. .leading or .trailing.
What is GeometryReader used for?
- Reading files
- Reading the size and coordinate space proposed to a view
- Drawing shapes only
- Networking
Answer: Reading the size and coordinate space proposed to a view. GeometryReader gives you a GeometryProxy with the proposed size and frame so you can lay out relative to it.
Inside GeometryReader, what does geometry.size.width give you?
- The screen brightness
- The width available to that GeometryReader
- The font size
- A random number
Answer: The width available to that GeometryReader. The GeometryProxy's size reports the width and height proposed to the GeometryReader.
A Spacer inside an HStack does what?
- Adds a fixed 10pt gap
- Removes spacing
- Adds a border
- Expands to push views apart, filling free space
Answer: Expands to push views apart, filling free space. Spacer is a flexible view that expands to fill available space, pushing siblings apart.
Continue this course
- Previous: @EnvironmentObject & the Environment
- Next: Lists, ForEach & Navigation — List, ForEach with Identifiable, and NavigationStack/NavigationLink
Frequently asked questions
What are VStack, HStack, and ZStack?
They are SwiftUI's core layout containers. VStack arranges its children vertically (top to bottom), HStack arranges them horizontally (left to right), and ZStack layers them back-to-front so they overlap. You nest these freely — a row inside a column inside an overlay — to build any layout.
How do I control spacing and alignment in a stack?
Pass the spacing parameter to set the gap between children, e.g. VStack(spacing: 12). Pass alignment to control how children line up across the perpendicular axis: in a VStack, alignment is horizontal (.leading, .center, .trailing); in an HStack it's vertical (.top, .center, .bottom).
What does the .frame modifier do?
.frame proposes a size for the view it modifies. You can give fixed dimensions like .frame(width: 120, height: 44), or use flexible constraints like .frame(maxWidth: .infinity) to let the view grow into the available space. It's the main tool for sizing views explicitly.
What is a Spacer?
A Spacer is a flexible, invisible view that expands to fill available space along its stack's axis, pushing its siblings apart. In an HStack, a Spacer between two views shoves them to opposite ends; in a VStack it pushes them top and bottom. It's the easiest way to distribute or align content.
When should I use GeometryReader?
Use GeometryReader when a view needs to size or position itself relative to the space available to it — for example making something half the parent's width, or reacting to the device's dimensions. It provides a GeometryProxy with the proposed size and coordinate frame. Reach for it only when stacks and frames can't express the layout, since it takes up all offered space by default.