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. 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 chosen its size as that 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:

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

            Image("barbarian")
        }
    }
}

If the stack took the height from its first child, the second child would have had to overflowed its bounds; as we see, it does not, and the stack has the height of the second child now that one is the tallest:

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:

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

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

            Image("barbarianIcon")

            Text("Barbarian")
        }
    }
}

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:

hstack with additional 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 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 choose their own sizes, derived from the sizes of their children

Stacks in Constrained Sizes

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: 300, height: 100)
    }
}

When 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

That seems to work great, the Text wrapped its content to allow the stack to fit within the space proposed by its parent frame.

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:

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

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

As expected the text is truncated:

hstack with truncated text

But what if we had more than one Text in our stack, which one would choose a smaller 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 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)
        .frame(width: 300, height: 100)
    }
}

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

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.

As we’ve already seen, the size of the stack is derived from the size of the children. But we also saw that the children received a proposed size from the stack.

When the stack lays out its children, like every view, it receives a proposed size from its parent; in our examples, a frame or the device.

Since the stack knows how many children it has, and their spacing, it subtracts the necessary spacing from that proposed space.

It then first offers this remaining proposed 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.

What remains is the amount of proposed space that can be used by the more flexible children. The stack divides this remainder up equally between them, and thus both Text views get an equal share of the remainder and 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)
        .frame(width: 300, height: 100)
    }
}

After subtracting from the proposed size for spacing, and for inflexible children, the stack offers the remaining space to those with a higher layout priority first, and then the remainder to those with a lower layout priority:

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, each child view chooses its own size, and. the vertical stack itself chooses its own 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 if the behavior matches the horizontal stack, just transposed in direction:

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

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

            Image("barbarianIcon")

            Text("Barbarian")
        }
    }
}

The stack grows vertically to accommodate the new children, as well as adding spacing between them:

vstack with additional children

This confirms that, 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

Note that the default spacing is not a fixed value, but a value that makes sense for the specific pair of children. In the example above, the stack has chosen more space between the hero’s name and the icon for the class, than between the icon for the class and the class name.

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

We can further test that the sizes of stacks cannot be overriden by placing the stack within a frame that would be insufficiently tall:

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

            Text("Nogistune Takeshi")
                .font(.title)
        }
        .frame(width: 200, height: 80)
    }
}

When constrained in width, the flexible Text children of a horizontal stack were able to choose a smaller size to help, but what happens when a vertical stack is constrained in height?

vstack with restricted frame

First we can see that the stack shrank horizontally as before, with the Text being flexible enough about choosing its size that it was able to truncate the string so that the stack can fit the proposed width.

But in the vertical direction the stack has failed to fit within the proposed size. 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 automatically reducing the font size is not one of its capabilities.

So what we see is the same behavior as when we place an image inside a too-small frame, it simply overflows its bounds. This confirms that stacks are just like other views, once they choose their own size, it cannot be overriden.

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.

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.