SwiftUI: stacks and groups
By Flavio Copes
Learn how to lay out multiple views in SwiftUI using HStack, VStack, and ZStack to align them on each axis, plus Group to bundle views together.
No SwiftUI app, beside an Hello World, has just a view.
When you want to add more than one view, you need to add them to a stack.
There are 3 kinds of stacks:
HStackaligns items on the X axisVStackaligns items on the Y axisZStackaligns items on the Z axis
Let’s go back to the Hello World app:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
To add a second Text view we can’t do this:
struct ContentView: View {
var body: some View {
Text("Hello World")
Text("Hello again!")
}
}
but we have to embed those views into a stack.
Let’s try with VStack:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
Text("Hello again!")
}
}
}

See? The views are aligned vertically, one after the other.
Here’s HStack:

And here’s ZStack, which puts items one in front of the other, and in this case generates a mess:

ZStack layers views on top of each other. The first child sits at the back. The last one sits on top.
A common use is a photo with a title over it:
ZStack {
Image("Avatar")
.resizable()
.scaledToFill()
Text("Flavio")
.font(.title)
.foregroundStyle(.white)
.padding(8)
.background(.black.opacity(0.6))
}
.frame(width: 200, height: 200)
.clipped()
The image fills the frame. The label sits on top in the center. Reach for ZStack when views share the same space. Use VStack or HStack when they sit next to each other.
In SwiftUI we organize all our UI using those 3 stacks.
Spacing and alignment
Stacks accept spacing and alignment.
spacing is the gap between children, in points:
VStack(spacing: 24) {
Text("Hello World")
Text("Hello again!")
}
alignment controls how children line up on the cross axis. On a VStack that is left, center, or right. On an HStack that is top, center, or bottom:
VStack(alignment: .leading, spacing: 8) {
Text("Profile")
.font(.headline)
Text("flaviocopes.com")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Both titles hug the leading edge. Without .leading, they would sit centered by default.
Nested stacks
Real screens mix stacks. An HStack inside a VStack is the usual pattern for a row with an icon and text:
VStack(alignment: .leading, spacing: 12) {
Text("Today")
.font(.title2)
HStack(spacing: 12) {
Image(systemName: "sun.max.fill")
.font(.largeTitle)
.foregroundStyle(.orange)
VStack(alignment: .leading) {
Text("Sunny")
Text("24°C")
.foregroundStyle(.secondary)
}
}
}
.padding()
Outer VStack stacks the heading and the weather row. Inner HStack puts the icon beside the temperatures. Nest until the layout matches the design. Stop when a child can be its own small view.
Spacer
Spacer eats free space inside a stack. Put one between two views and they push apart:
HStack {
Text("Status")
Spacer()
Text("Online")
.foregroundStyle(.green)
}
.padding()
“Status” stays on the left. “Online” goes to the right. Without the Spacer, both texts would sit next to each other in the center.
A common mistake is stacking several Spacers and wondering why the layout feels loose. One Spacer is usually enough. Prefer a fixed spacing value when the gap should stay the same on every device size.
Group
We also use Group, a view that, similarly to stacks, can be used to group together multiple views, but contrary to stack views, it does not affect layout.
VStack {
Group {
Text("Hello World")
Text("Hello again!")
}
}
Group is for shared modifiers and logical grouping. Put related views in a Group, then apply one environment modifier to that group. It does not invent layout of its own.
Older tutorials still say views can only have 10 children, and that you need Group to work around that. That hard limit is gone in Swift 5.9 and later: ViewBuilder uses parameter packs, so a stack can hold more than 10 direct children. You still reach for Group when you want one place to hang shared modifiers, not because you hit a child-count wall.
Lazy stacks
If the stack is long and scrolling, prefer LazyVStack or LazyHStack inside a ScrollView. Those build rows as they appear on screen, which keeps memory lower than a plain VStack with hundreds of children:
ScrollView {
LazyVStack(alignment: .leading, spacing: 12) {
ForEach(1...200, id: \.self) { number in
Text("Row \(number)")
.padding(.horizontal)
}
}
}
Use a plain VStack for short, fixed layouts. Switch to LazyVStack when you scroll a long list of rows and you do not need List features like swipe actions or selection.
Environment modifiers
Group and the stack views are views too, and so they have modifiers.
Sometimes modifiers affect the view they are applied to, like in this case:
Text("Hello World")
.font(.largeTitle)
Sometimes however they are used to apply the same property to multiple views at the same time.
Like this:
VStack {
Text("Hello World")
Text("Hello again!")
}
.font(.largeTitle)

See? By applying the font() modifier to the VStack, the .largeTitle font was applied to both Text views.
This is valid for modifiers that we call environment modifiers. Not every modifier can work this way, but some do, like in the above example.
Want me to talk about your product? You can sponsor this site.
Related posts about swift: