Layout with Stacks in SwiftUI

SwiftUI uses a declarative approach to creating the User Interface. This means the code is written to state what the user interface should look like. Stacks provide a mechanism to define the layout arrangement of multiple views in a SwiftUI app.

Stacks are used in SwiftUI to layout views instead of using auto layout. There are three types of stacks:

  • VStack - Arrange views in the stack vertically
  • HStack - Arrange views in the stack horizontally
  • ZStack - Arrange views in the stack overlaid on top of each other

All three types of stacks are used in this article to layout the following App.

layout using stacks
Target layout using Stacks in SwiftUI



Stacks

The VStack, HStack and ZStack are simple views that layout their containing views. The VStack arranges its child views in a vertical stack, while the HStack arranges them horizontally. The ZStack lays down each of it's child views on top of each other. These stacks can be combined in numerous nested combinations to create a variety of layouts.

SwiftUI layout examples with stacks
SwiftUI layout examples with Stack Views



VStack

The heading in the desired layout consists of two Text Views arranged vertically. These are laid out using a VStack and the order written is the order the views are added to the VStack.

1VStack(alignment: .leading, spacing: 5){
2    Text("Hobbies")
3        .font(.system(.largeTitle, design:.rounded))
4        .fontWeight(.black)
5    Text("The quest for a balanced life")
6        .font(.title2)
7}

Two Text Views combined in a VStack
Two Text Views combined in a VStack



HStack

The heading and sub heading are arranged correctly with respect to each other, but they are in the center of the screen horizontally and vertically. To arrange these to the left side of the screen a HStack and a Spacer are used. The Spacer is a flexible space that expands to fill the containing Stack. Another VStack and Spacer is used to ensure the heading is displayed at the top of the screen.

 1VStack {
 2    HStack {
 3        VStack(alignment: .leading, spacing: 5){
 4            Text("Hobbies")
 5                .font(.system(.largeTitle, design:.rounded))
 6                .fontWeight(.black)
 7            Text("The quest for a balanced life")
 8                .font(.title2)
 9        }
10        Spacer()
11    }
12
13    Spacer()
14}

Nested stacks and spacers to layout header
Nested stacks and spacers to layout header

cmd+click can be used in Xcode to display a context menu allowing you to select Embed in HStack.

`cmd+click` on a view to show popup option to embed in HStack
cmd+click on a view to show popup option to embed in HStack



Extracting Views

There is also a menu option Extract Subview which will take all the nested code and move it to a new View. This is a great way to refactor code and break down the code into smaller reusable chunks. Now the main view contains just one View (HeaderView) in the main VStack.

1VStack {
2    HeaderView()
3    Spacer()
4}
 1struct HeaderView: View {
 2    var body: some View {
 3        HStack {
 4            VStack(alignment: .leading, spacing: 5){
 5                Text("Hobbies")
 6                    .font(.system(.largeTitle, design:.rounded))
 7                    .fontWeight(.black)
 8                Text("The quest for a balanced life")
 9                    .font(.title2)
10            }
11            Spacer()
12        }
13    }
14}

Extracted header view
Extracted header view into a separate struct



Card View

Start by creating one card for Books. This consists of an image, which uses sf-symbols for the image. Apple provide SF Symbols as a set of over 2,400 highly configurable symbols that integrate with the San Francisco system font. Below the image is a VStack containing the two text views. The image and the text views stack is embedded further in a VStack, which is modified with a frame and background color.

 1VStack(spacing: 20) {
 2    Image(systemName: "character.book.closed.fill")
 3        .font(.system(size: 60))
 4        .rotation3DEffect(
 5            .degrees(20),
 6            axis: (x: 1.0, y: 0.0, z: -0.5)
 7        )
 8    VStack(spacing: 10) {
 9        Text("Books")
10            .font(.system(.title2, design:.rounded))
11            .fontWeight(.black)
12        Text("read, dream, believe, live")
13            .font(.body)
14            .fontWeight(.bold)
15            .lineLimit(2)
16            .multilineTextAlignment(.center)
17    }
18}
19.frame(minWidth: 0, maxWidth: .infinity, minHeight: 200, maxHeight: 250)
20.padding(10)
21.background(Color(red: 200/255, green: 200/255, blue: 230/255))
22.cornerRadius(20)

VStack, Image and Text views to create card layout for books
VStack, Image and Text views to create card layout for books



Extract Subview and pass in values

The code to layout the Card is extracted to a Subview in order to keep the code in the main view simpler. Three variables are created in the CardView struct, so that the CardView can be reused to display the cards for different hobbies. The specific details of the icon name and the text to display are passed in as arguments to the CardView constructor. The layout of the Books card looks the same, but the code is refactored into a separate Subview.

 1var body: some View {
 2    VStack {
 3        HeaderView()
 4
 5        CardView(
 6            iconName: "character.book.closed.fill",
 7            title: "Books",
 8            description: "read, dream, believe, live"
 9        )
10
11        Spacer()
12    }
13}
 1struct CardView: View {
 2    var iconName: String
 3    var title: String
 4    var description: String
 5
 6    var body: some View {
 7        VStack(spacing: 20) {
 8            Image(systemName: iconName)
 9                .font(.system(size: 60))
10                .rotation3DEffect(
11                    .degrees(20),
12                    axis: (x: 1.0, y: 0.0, z: -0.5)
13                )
14            VStack(spacing: 10) {
15                Text(title)
16                    .font(.system(.title2, design:.rounded))
17                    .fontWeight(.black)
18                Text(description)
19                    .font(.body)
20                    .fontWeight(.bold)
21                    .lineLimit(2)
22                    .multilineTextAlignment(.center)
23            }
24        }
25        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 200, maxHeight: 250)
26        .padding(10)
27        .background(Color(red: 200/255, green: 200/255, blue: 230/255))
28        .cornerRadius(20)
29    }
30}

Extracted cardView for books
Extracted cardView for books



Add multiple cards

It is easy now to add more cards and arrange them using stacks. The maxHeight for the card was also adjusted to 200. This only needs to be changed in one place in the CardView, and all cards in the app are updated. The maxWidth of the cards being set to infinity causes the Horizontal stack to display the two cards with equal width. If there were three or four cards added, they would all divide up the available width equally.

 1VStack {
 2    HeaderView()
 3
 4    HStack {
 5        CardView(
 6            iconName: "character.book.closed.fill",
 7            title: "Books",
 8            description: "read, dream, believe, live"
 9        )
10
11        CardView(
12            iconName: "music.quarternote.3",
13            title: "Music",
14            description: "listen, sing, dance"
15        )
16    }
17
18    CardView(
19        iconName: "laptopcomputer.and.iphone",
20        title: "Programming",
21        description: "imagine, create, share, enjoy"
22    )
23
24    Spacer()
25}
26.padding()

three cards in stacks
Three cards added in stacks



ZStack

ZStack is used to display views on top of each other. The order in which the views are added to a stack determines the order the views are rendered. If the CardView was moved to the end of the ZStack, it would be rendered last and would appear on top of the two BadgeViews. The BadgeView is created to describe the code for the overlay views and this allows for reuse and keeps the main view simpler.

 1ZStack {
 2    CardView(
 3        iconName: "laptopcomputer.and.iphone",
 4        title: "Programming",
 5        description: "imagine, create, share, enjoy"
 6    )
 7
 8    BadgeView(title: "SwiftUI", padding: 5, radius: 10, rotation: -35, xOffset: -125, yOffset: -70)
 9
10    BadgeView(title: "Layout using Stacks", padding: 10, radius: 20, rotation: 0, xOffset: 0, yOffset: 110)
11
12}
 1struct BadgeView: View {
 2    var title: String
 3    var padding: CGFloat
 4    var radius: CGFloat
 5    var rotation: Double
 6    var xOffset: CGFloat
 7    var yOffset: CGFloat
 8
 9    var body: some View {
10        Text(title)
11            .font(.callout)
12            .fontWeight(.bold)
13            .padding(padding)
14            .background(Color(red: 20/255, green: 18/255, blue: 237/255))
15            .foregroundColor(.white)
16            .cornerRadius(radius)
17            .rotationEffect(.degrees(rotation))
18            .offset(x: xOffset, y: yOffset)
19    }
20}

Overlaying views with ZStack
Overlaying views with ZStack



Final Code

Here is the final code to create the layout as required. The use of sub views allows the main code to be kept simpler and reuse of code for greater consistency.

The main view:

 1struct StackLayout: View {
 2    var body: some View {
 3        VStack {
 4            HeaderView()
 5
 6            HStack {
 7                CardView(
 8                    iconName: "character.book.closed.fill",
 9                    title: "Books",
10                    description: "read, dream, believe, live"
11                )
12
13                CardView(
14                    iconName: "music.quarternote.3",
15                    title: "Music",
16                    description: "listen, sing, dance"
17                )
18            }
19
20            ZStack {
21                CardView(
22                    iconName: "laptopcomputer.and.iphone",
23                    title: "Programming",
24                    description: "imagine, create, share, enjoy"
25                )
26
27                BadgeView(title: "SwiftUI", padding: 5, radius: 10, rotation: -35, xOffset: -125, yOffset: -70)
28
29                BadgeView(title: "Layout using Stacks", padding: 10, radius: 20, rotation: 0, xOffset: 0, yOffset: 110)
30            }
31
32            Spacer()
33        }
34        .padding()
35    }
36}

The HeaderView:

 1struct HeaderView: View {
 2    var body: some View {
 3        HStack {
 4            VStack(alignment: .leading, spacing: 5){
 5                Text("Hobbies")
 6                    .font(.system(.largeTitle, design:.rounded))
 7                    .fontWeight(.black)
 8                Text("The quest for a balanced life")
 9                    .font(.title2)
10            }
11            Spacer()
12        }
13    }
14}

The CardView:

 1struct CardView: View {
 2    var iconName: String
 3    var title: String
 4    var description: String
 5
 6    var body: some View {
 7        VStack(spacing: 20) {
 8            Image(systemName: iconName)
 9                .font(.system(size: 60))
10                .rotation3DEffect(
11                    .degrees(20),
12                    axis: (x: 1.0, y: 0.0, z: -0.5)
13                )
14            VStack(spacing: 10) {
15                Text(title)
16                    .font(.system(.title2, design:.rounded))
17                    .fontWeight(.black)
18                Text(description)
19                    .font(.body)
20                    .fontWeight(.bold)
21                    .lineLimit(2)
22                    .multilineTextAlignment(.center)
23            }
24        }
25        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 200, maxHeight: 200)
26        .padding(10)
27        .background(Color(red: 200/255, green: 200/255, blue: 230/255))
28        .cornerRadius(20)
29    }
30}

The BadgeView:

 1struct BadgeView: View {
 2    var title: String
 3    var padding: CGFloat
 4    var radius: CGFloat
 5    var rotation: Double
 6    var xOffset: CGFloat
 7    var yOffset: CGFloat
 8
 9    var body: some View {
10        Text(title)
11            .font(.callout)
12            .fontWeight(.bold)
13            .padding(padding)
14            .background(Color(red: 20/255, green: 18/255, blue: 237/255))
15            .foregroundColor(.white)
16            .cornerRadius(radius)
17            .rotationEffect(.degrees(rotation))
18            .offset(x: xOffset, y: yOffset)
19    }
20}

final layout with VStack, HStack, ZStack and sub views
Final layout with VStack, HStack, ZStack and sub views



Conclusion

The SwiftUI framework uses Stacks to control the layout of views instead of Auto Layout. VStack arranges its child views in a vertical stack, HStack arranges them horizontally and ZStack arranges them on top of each other in the direction of the viewer. The Stacks can be nested and combined to create the desired App layout. Spacer is used to fill out available space to ensure views line up correctly. It is possible to quickly try different layouts with the use of Stacks and see the results in the preview as well as run different simulations. The resulting layout is easier to change and finalise without unexpected side effects than use of Auto Layout.