Stacks

The three most important layout tools in your SwiftUI toolbox are the three kinds of stacks: horizontal, vertical, and z-axis (yes, there really is no good adjective for this one).

These views are parents to multiple children, and in order to fully understand how these lay them out, you should first understand that views choose their own sizes.

Horizontal Stack

A horizontal stack is created using an HStack and a view builder that specifies its children:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")

            Text("Nogistune Takeshi")
                .font(.title)
        }
    }
}

It lays out its children sequentially from its leading edge to its trailing edge:

hstack

We can learn a lot by extending and constraining this simple example.

The first child is an Image which chooses its size to be the size of its source image. The second child is a Text which has chosen its size as that needed to render its string in the chosen font.

The Text is placed horizontally in the stack after the image, with space between them. We can specify the amount of space we want with the optional spacing parameter to the HStack, or we omit the parameter as we did here to allow the stack to choose the spacing.

When we allow the stack to choose the spacing, it does not choose a fixed value, but a value that makes sense for each specific pair of children.

Since the Text is not as tall as the Image, the stack vertically centers it. This is an entire topic in of itself, and you can read more in alignments. For now we’ll stick to the default centering behavior.

The width of the stack is the sum of the widths of its children, plus the spacing between them.

In the example above the stack has the height of the first child, the Image, which also happens to be the tallest. We can verify whether the height of the stack is that of the first child, or the tallest, by switching the order:

struct ContentView : View {
    var body: some View {
        HStack {
            Text("Nogistune Takeshi")
                .font(.title)

            Image("barbarian")
        }
    }
}

If the stack took the height from the first child, the Image would have had to overflowed the stack’s bounds; as we see, it does not:

hstack with the children in reverse order

This confirms that the height of the stack is the height of the tallest child. When we look at alignments we’ll learn that the height of the stack also includes the vertical positions of its children, thus a more general rule is that the stack’s bounds are the union of the bounds of its children.

the stack’s bounds are the union of the bounds of its children

We can confirm that the stack’s bounds are derived from the children by looking at what happens if the size of the device is larger than the combined sizes of the stack’s children:

hstack in larger frame

The stack does not expand to fill the size proposed by the device, its parent. The children of the stack chose their own sizes, and stacks are no different. Stacks choose their own sizes, derived from the sizes of their children.

Stacks when Constrained in Size

We know that views choosing their own sizes doesn’t mean that they can’t be flexible about the size that they choose, and we saw in that post that Text can return different sizes by truncating, tightening, and wrapping its string.

Let’s take a look at what happens when there is not enough space available for all of the children of the stack to be laid out horizontally.

We can do this by adding a .frame to the stack, which as we should recall, creates a new view with the stack as a child, and proposes the size given to it to the stack:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")

            Text("Nogistune Takeshi")
                .font(.title)
        }
        .frame(width: 280, height: 100)
    }
}

Since there is not enough space for both the Image and the Text to be laid out horizontally, the Text can help out by wrapping its contents, reducing its width by increasing its height:

hstack with wrapped text

But what if we had more than one Text in our stack, which one would adjust its size?

To see what happens, let’s divide our single Text into two, one each for the character’s family name and given name respectively. Since we want the Text to be truncated rather than wrapped, we’ll also throw in a .lineLimit:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")

            Text("Nogistune")
                .font(.title)
            Text("Takeshi")
                .font(.title)       
        }
        .lineLimit(1)
        .frame(width: 280, height: 100)
    }
}

Rather than allowing the first Text to take up most of the space, and the second to extra-truncated to fit, the stack instead divides the space up fairly between them and causes both to truncate:

hstack with two truncated txt

Yet while the space has been fairly divided between the Text children, the Image doesn’t seem to have been affected.

Proposed Sizes in Stacks

We gave a warning in views choose their own sizes to not confuse the proposed size a view receives from its parent with the size the parent chooses for its own, and now we can review why.

Like every view the stack receives a proposed size from its parent; in our examples, a frame or the device. And as we’ve already seen, the size of the stack is derived from the size of its children.

When the stack lays out its children, they receive a proposed size from the stack, but that proposed size cannot be the size of the stack since it has not been chosen yet. Neither is it the proposed size the frame received from its parent, which would not fairly distribute space as we’ve seen the stack does.

Instead the stack makes proposed size offers to its children in order of their increasing horizontal flexibility.

The stack begins with the proposed size it received from its own parent, and since it knows how many children it has and how much space to place between them, it can subtract the sum of the necessary spacing to obtain the space available for all of its children, which it can then divide equally

As the least flexible child, the Image receives the first offer of a proposed size. Since the Image is inflexible, it chooses a size without considering this. The stack subtracts the chosen size from the available space, and then divides the remainder equally again.

As the more flexible children, the two Text views next receive an offer of a proposed size, taking their equal share of the remainder and truncating equally.

In flexible frames we’ll learn how we can adjust a view’s flexibility by specifying minimum and maximum lengths.

Layout Priorities

If we want a little more control, for example if we want the character’s family name to be less likely to be truncated than their given name, stacks give us a way to control the order in which children are laid out.

By increasing the layout priority of certain children, the stack introduces an extra layout pass for those children before their siblings:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")

            Text("Nogistune")
                .font(.title)
                .layoutPriority(1)
            Text("Takeshi")
                .font(.title)       
        }
        .lineLimit(1)
        .frame(width: 280, height: 100)
    }
}

After subtracting spacing from the proposed size, the stack will consider children in order of decreasing layout priority. For those of the same layout priority, the stack as before will make offers in order of their increasing horizontal flexibility.

So in this example we’ve increased the priority of the Text containing the family name, ensuring that is laid out first, and gets the space it needs.

Next the two remaining children with a default zero layout priority are considered. Since the Image is the least flexible, that receives the first offer and chooses its size. Finally the Text containing the given name is proposed the remainder.

hstack with layout priority truncating second text only

Vertical Stack

The vertical stack is very similar to the horizontal, it is defined with VStack:

struct ContentView : View {
    var body: some View {
        VStack {
            Image("barbarian")

            Text("Nogistune Takeshi")
                .font(.title)
        }
    }
}

Rather than laying out its children horizontally, it lays out its children sequentially from its top edge to its bottom edge:

vstack

Just like the horizontal stack, each child view chooses its own size, and. the vertical stack itself chooses its own size, derived from the sizes of its children.

Vertical stacks obey all of the same rules and constraints as horizontal stacks, just in a perpendicular axis.

Since it’s arranging its children vertically, it’s the width of the vertical stack that is the width of its largest child. Smaller children are positioned horizontally within that space, with the default alignment to be centered.

Vertical stacks can be particularly useful to demonstrate the default spacing behavior, for example if we split the surname and family name as we did for a horizontal stack example above:

struct ContentView : View {
    var body: some View {
        VStack {
            Image("barbarian")

            Text("Nogistune")
                .font(.title)
            Text("Takeshi")
                .font(.title)
        }
    }
}

The result includes spacing between the Image and first Text child, but no spacing between the two adjacent Text children:

vstack with no spacing between text

Z-Axis Stack

The third kind of stack is neither horizontal or vertical, but instead overlays its children on top of each other. It is defined using ZStack:

struct ContentView : View {
    var body: some View {
        ZStack {
            Image("barbarian")

            Text("Nogistune Takeshi")
                .font(.title)
        }
    }
}

The children are overlaid sequentially, with the first defined on the bottom and the last defined on top:

stack

Just like the horizontal and vertical stacks, each child view chooses its own size, and the z-axis stack itself chooses its own size too, derived from the sizes of its children.

While it might seem limited in usefulness, the z-axis stack can actually be one of the most powerful tools for combining views, and is one of the fundamentals for creating new controls, or overlaying shapes.

In this simple example all of the children are laid over top of each other. We’ll learn when we look at alignments that we can move them apart from each other, and in exploding stacks how we can combine alignments and flexible frames inside a z-axis stack.

As with the horizontal and vertical stacks, the bounds of a z-axis stack are the union of the bounds of its children; but with an added twist: only the bounds of its children with the highest layout priority. We’ll look at this in more detail when we look at secondary views.

Combining Stacks

More complex layouts can be created by combining stacks together, a topic complex enough to deserve its own post.


Imagery used in previews by Kaiseto, original images and derived here licensed under Creative Commons 3.0 BY-NC-SA.