SwiftUI: stacks and groups

By

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:

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!")
    }
  }
}

iPhone simulator showing VStack with Hello World and Hello again! text views arranged vertically

See? The views are aligned vertically, one after the other.

Here’s HStack:

iPhone simulator showing HStack with Hello World Hello again! text views arranged horizontally

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

iPhone simulator showing ZStack with overlapping Hello World text views creating messy display

ZStack puts the views one on top of the other. The first child ends up at the back, the last one in front.

The classic use case is a background image with some text 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 200 by 200 frame and the label is drawn over it, centered. Use ZStack when views share the same space, VStack and HStack when they sit next to each other.

In SwiftUI we organize all our UI using those 3 stacks.

Spacing and alignment

Stacks accept two parameters, spacing and alignment.

spacing is the gap between the children, in points. Without it, SwiftUI picks a default that depends on the platform:

VStack(spacing: 24) {
  Text("Hello World")
  Text("Hello again!")
}

alignment controls how the children line up on the other axis. In a VStack that means left, center or right (.leading, .center, .trailing). In an HStack it means top, center or bottom:

VStack(alignment: .leading, spacing: 8) {
  Text("Profile")
    .font(.headline)
  Text("flaviocopes.com")
    .font(.subheadline)
    .foregroundStyle(.secondary)
}

Both texts start at the left edge. The default is .center, and with texts of different lengths that looks odd.

Nested stacks

Real screens mix stacks. An HStack inside a VStack is how you build a row with an icon and some text next to it:

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()

The outer VStack puts the heading above the weather row. The inner HStack puts the icon beside the two texts, which are in a VStack of their own. When a nested block starts to grow, move it to its own view.

Spacer

Spacer takes up all the free space in a stack. Put one between two views and it pushes them apart:

HStack {
  Text("Status")
  Spacer()
  Text("Online")
    .foregroundStyle(.green)
}
.padding()

“Status” goes to the left edge, “Online” to the right. Without the Spacer, the two texts would sit next to each other in the center.

If you add more than one Spacer, they share the free space equally. That’s how you center something, one Spacer on each side. When the gap has to be the same on every screen size, use a fixed spacing value instead.

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 useful to apply one modifier to several views at once, as we’ll see below, and to keep related views together. It adds no layout of its own.

When I first wrote this post, a view could have at most 10 children, and Group was the way around that limit. Since Xcode 15 (Swift 5.9) the limit is gone: ViewBuilder uses parameter packs, so a stack can have as many direct children as you want. Today you use Group for the shared modifiers, not for the count.

Lazy stacks

A VStack creates all its children right away. With 200 rows inside a ScrollView that’s wasted work, because most of them are off screen. LazyVStack and LazyHStack create the children only when they are about to become visible:

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. Use LazyVStack for a long scrolling list of rows, when you don’t need what List gives you, 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)

iPhone simulator showing VStack with both text views displaying in large title font size

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.

Tagged: Swift · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about swift: