VStack, HStack, and ZStack in SwiftUI | SwiftUI Bootcamp #10
Stacks are the primary layout containers in SwiftUI — nearly every screen you build will combine VStack, HStack, and ZStack in some way. After this lesson you'll understand the axis each stack controls, how their spacing and alignment parameters work, and the difference between using ZStack versus .background().
What You'll Learn
- The three stack types and their layout axes: Vertical, Horizontal, and Depth (Z-order)
- How
ZStackand.background()differ in their effect on the parent view's size - The
spacingandalignmentparameters and how they shape stack layout
Mental Model
Stacks are like organizers for views. A VStack is a vertical stack of plates — each plate sits on top of the previous one, and the stack grows downward. An HStack is a horizontal shelf — items sit side by side and the shelf grows wider. A ZStack is a stack of glass slides under a microscope — views sit directly on top of each other, centered by default, and the whole stack is as large as the largest slide.
The sample shows something important: you can place text over a circle in two different ways — with ZStack (the text and circle are separate children of the same stack) or with .background() (the circle is attached to the text view as a background layer). Both produce the same visual, but they behave differently when the layout changes.
Detailed Explanation
VStack(alignment:spacing:) stacks views vertically. alignment controls horizontal positioning of children within the stack (.leading, .center, .trailing). spacing sets the gap between adjacent children. Default spacing uses the system value (~8pt), and passing spacing: 0 removes all gaps.
HStack(alignment:spacing:) stacks views horizontally. alignment controls vertical positioning of children (.top, .center, .bottom, .firstTextBaseline). The baseline alignments are especially useful for mixed font sizes where you want the text baselines to align.
ZStack(alignment:) layers views in Z-order — the first child is deepest, the last is on top. The alignment parameter controls where all children are anchored within the stack's bounding box. It defaults to .center. The stack's size is the bounding box of all children combined.
A key architectural decision is whether to use ZStack for layering or to use .background()/.overlay(). The difference: ZStack sizes itself to the largest child, so adding a large background shape expands the whole stack. .background() and .overlay() don't change the layout size of the primary view — they're purely cosmetic layers that respect the primary view's frame.
Code Structure
The sample is in StacksBootcamp.swift and demonstrates a VStack containing two equivalent approaches to placing text over a circle. The first uses an explicit ZStack, the second uses .background(). Comparing them side by side reveals the subtle difference between these two layering approaches.
Complete Code
StacksBootcamp.swift
import SwiftUI
struct StacksBootcamp: View {
// Vstacks -> Vertical
// Hstacks -> Horizontal
// Zstacks -> zIndex (back to front)
var body: some View {
VStack(spacing: 50) { // arranges children vertically with 50pt gaps between them
ZStack { // layers children back-to-front; Circle() is behind, Text("1") is in front
Circle()
.frame(width: 100, height: 100) // explicit size so the circle doesn't expand to fill
Text("1")
.font(.title)
.foregroundColor(.white) // white so it's visible against the default dark circle
}
Text("1")
.font(.title)
.foregroundColor(.white) // same white text, but this time the circle is a background layer
.background(
Circle()
.frame(width: 100, height: 100) // circle is visual-only; text view's size stays the same
)
}
}
}
struct StacksBootcamp_Previews: PreviewProvider {
static var previews: some View {
StacksBootcamp()
}
}Code Walkthrough
VStack(spacing: 50)— Creates the outer vertical layout with 50pt gaps. The two label groups sit 50pt apart. Withoutspacing: 50, they'd use the system default (~8pt) and appear much closer.- First child:
ZStack { ... }— TheZStackcontains two independent children: theCircleat the bottom layer and theTextat the top layer. The stack's size is 100×100 (the size of the circle) because the text is smaller. Both views are centered within that 100pt box. Circle().frame(width: 100, height: 100)— The explicit frame is important here. Without it, the circle would expand to fill all space theZStackoffers, making the circle as large as the screen. Always frame shapes inZStackunless you want them to expand.- Second child:
Text("1").background(Circle()...)— Visually identical to the first, but architecturally different. TheTextview is the primary view; theCircleis its background. TheTextcontrols the layout size; the circle is just paint behind it. - Size comparison — The
ZStackapproach makes the layout item 100×100 (the circle's size). The.background()approach makes the layout item the size of theText("1")— roughly 24×28pt. Add.background(Color.yellow)to each to see the difference clearly.
Common Mistakes
Mistake: Putting a full-screen background color in a ZStack and watching the content disappear When you place a Color.blue in a ZStack without a frame, the color expands to fill all available space — 100% of the screen. This is intentional for background screens. But if you also put content in the same ZStack, make sure the content is listed after the background color so it renders on top.
Mistake: Expecting VStack to fill the full screen widthVStack sizes itself to its widest child by default — it doesn't expand to fill available horizontal space. To make a VStack full-width, add .frame(maxWidth: .infinity) to it, or add .frame(maxWidth: .infinity) to the widest child.
Mistake: Using ZStack when .background() or .overlay() is the right tool Reach for ZStack when you have multiple independent views that should all participate in layout sizing (e.g., a full-screen background behind scrolling content). Reach for .background() or .overlay() when you want to attach decorative layers to a specific view without affecting the layout of its siblings.
Key Takeaways
VStackarranges vertically,HStackhorizontally,ZStackby depth — they share the samealignmentandspacingparameters but apply them on different axesZStacksizes to its largest child;.background()and.overlay()don't change the primary view's layout size- Always apply
.frame()to shapes inside aZStackto prevent them from expanding to fill the entire container
Last updated: June 27, 2026