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 have fixed 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 is fixed in size to the size of its source image. Of the two children, this is the tallest, and we can see that the stack itself has this same height; as we’ll confirm shortly, the height of the horizontal stack is the height of its largest child.

the height of the horizontal stack is the height of its largest child

The second child is a Text which has the fixed size needed to render its string in the chosen font. It’s placed horizontally in the stack after the image, with space between them. A default spacing is used, and this can be changed with the spacing parameter to the HStack.

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.

No spacing is provided before the first child, or after the last child, only between them; thus the width of the horizontal stack is the sum of the widths of each of its children, plus the spacing between them.

the width of the horizontal stack is the sum of the widths of each of its children, plus the spacing between them

We can demonstrate that the height of the stack is the height of its largest child, not just the first child, by changing the order of its children:

hstack with the children in reverse order

With that confirmed, let’s now see what happens if we add more items to the stack. We’ll add both an icon and text label for the character’s class:

hstack with additional children

The stack simply expands in width to accommodate its new children. Each new child is placed after the previous sibling, which means that the horizontal position of a child in a horizontal stack is controlled only by the widths of the previous children.

the horizontal position of a child in a horizontal stack is controlled only by the widths of the previous children

This is a pretty important detail to learn; if you want to change the vertical position of a child in a horizontal stack, you use alignment, but if you want to change its horizontal position, you instead need to change the sizes of its siblings.

We haven’t shown the layout outside of the stacks in any of these examples, let’s see what happens if the size of the parent is larger than the combined sizes of the stack’s children:

hstack in larger frame

The stack does not expand to fill the space of its parent. The children of the stack are fixed in size, and stacks are no different, stacks are fixed in size, derived from the sizes of their children

stacks are fixed in size, derived from the sizes of their children

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

When there is not enough space in the parent of the stack for both the Image and the Text, the Text can help out by wrapping its contents, reducing its width by increasing its height:

hstack with wrapped text

That seems to work great, the Text wrapped its content to allow the stack to fit within its parent. If there isn’t enough room for it to wrap the content, or we add a lineLimit the Text can decrease its width without increasing its height by truncating its content instead:

hstack with truncated text

But what if we had more than one Text in our stack? 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 don’t want the text to wrap, we’ll also throw in a lineLimit as we did above:

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

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

The result might be a little surprising, we might have expected the first Text to take up most of the space, and the second to be extra-truncated, but the stack has divided the space up fairly between them and caused them both to truncate:

hstack with two truncated txt

When the stack lays out its children, like every view, it receives its available space from its parent. Since it knows how many children it has, and their spacing, it subtracts the necessary spacing from the available space, and then divides that equally amongst its children.

It then first offers this equal space to its children that are inflexible in size, we’ll dive more into this in flexible frames, for now it’s enough to know that’s the Image. Since this uses a lot less of the width than is available in its equal part, the stack can divide the remainder up between the rest of its children.

Both Text views then get an equal share of the remainder, and as a result truncate equally.

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, we can introduce an additional pass to the layout of a stack by increasing the layout priority of the children we want in the extra pass.

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

            Text("Nogistune")
                .font(.title)
                .layoutPriority(1)
            Text("Takeshi")
                .font(.title)       
        }
        .lineLimit(1)
    }
}

After subtracting space for spacing, and for inflexible children, the stack offers the remaining space to those with a higher layout priority first:

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, as we’d expect, it instead lays out its children sequentially from its top edge to its bottom edge:

vstack

Just like the horizontal stack, since each child view is fixed in size, the vertical stack itself is also fixed in size, derived from the sizes of its children.

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.

the width of the vertical stack is the width of its largest child

By adding more children we can confirm that the behavior matches the horizontal stack, just transposed in direction:

vstack with additional children

And again, transposed from the horizontal, the height of the vertical stack is the sum of the heights of each its children, plus the spacing between them.

the height of the horizontal stack is the sum of the heights of each of its children, plus the spacing between them

As with the horizontal, the vertical stack does not expand to fill its parent frame, it retains the fixed size derived from its children and is positioned by its parent if it’s too small.

When the parent is too small for the stack content, it follows the same approach as the horizontal stack, first allocating the bounds for spacing, and then the size of inflexible views. The stack in the following example is placed in a smaller frame, bordered in gray:

vstack with restricted frame

We can see that the stack shrank horizontally as before, with the Text being flexible enough about its fixed size that it was able to truncate the string so that the stack can fit the frame.

But in the vertical direction the stack has failed to fit the bounds of its parent. When we discussed this for the horizontal we referred to the Image as inflexible in size, and Text as flexible, and while that’s true for the horizontal size, Text is just as inflexible in the vertical since reducing the font size is not one of its options.

So what we see is the same behavior as when we place an image inside a too-small parent, it simply overflows its bounds. This confirms that stacks are just like other views, and have fixed sizes.

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 is fixed in size, and the z-axis stack itself is also fixed in size, derived from the sizes of its children.

The height of the stack is the height of its tallest child. Less tall children are positioned vertically within that space.

The width of the stack is the width of its widest child. Smaller children are positioned horizontally within that space.

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.

Z-axis stacks are also particularly effective when used with custom alignments.

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.