Borders

It seems a little odd to write a post about borders, since every post so far has already used them without calling them out. In all of the examples I’ve added borders to views and used the result in the previews, rather than drawing them in by hand.

But I haven’t actually shown the code to do that, and it turns out that they’re slightly more interesting than you might expect.

There is a single method for specifying the border for a view:

func border<S>(_ content: S, width: CGFloat = 1) -> some View where S : ShapeStyle

The first parameter is required and specifies a shape style, there’s a quite a few options for that, but fortunately Color confirms to the ShapeStyle protocol so for the simplest cases all we need to do is specify a color.

The second parameter is optional and specifies the width of the border, defaulting to a single pixel.

So it wouldn’t be a surprise that to draw a single pixel yellow border around a Text we would use code like this:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .border(Color.yellow)
    }
}

And this produces the same example I’ve used before, except with the border that’s always been in the preview actually stated in code now:

text with a border

But now time for the first surprise.

You might expect that a border works a lot like a frame or padding, adding a view around the Text with enough space to draw the border, and positioning the child inside it.

Except they don’t, we can demonstrate this by increasing the width of the border:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .border(Color.yellow, width: 4)
    }
}

If this worked like padding, the border would increase in width around the text; instead we see something quite different:

text with a thick border

Instead of surrounding the text, the border has overlaid it. In fact, .border creates a secondary view on its child, and draws the border overlaid on top of it.

.border creates a secondary view on its child, and draws the border overlaid on top of it

If we wanted the border around the view instead, we have to combine it with .padding:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .padding(4)
            .border(Color.yellow, width: 4)
    }
}

This creates the Text view, and then .padding creates another view around that with additional padding added, and then .border adds a secondary view to the padding view, and draws overlaid on that:

text with thick border and padding

It’s important to note the distinction that the border is on the padding view; combined effects can be performed by carefully placing the overlays in the correct place:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .border(Color.red)
            .padding(4)
            .border(Color.yellow, width: 4)
            .border(Color.red)
    }
}

Here we create a red border overlaid on the Text, and then use padding to draw a thicker yellow border around the Text, and finally overlaid another red border onto the padding:

text with three borders

The total border width is 5px since it includes the additional pixel-wide border overlaid on the Text, or put another way, the yellow part of the border is 3px wide since the outer pixel is overlaid by the red border added to it.

Geometry Reader

For most layout needs we can combine stacks and flexible frames, allowing us to make views and controls put together from fixed size primitives views upwards.

For more complex layout needs, another option is to use GeometryReader. This is a construct that acts like an infinite frame, proposing the size of its parent to its children, and choosing its parent size as its own.

As an added feature, it passes the proposed size received to the builder as a closure argument. For example we can layout an image at a maximum of half of the size of the parent, while maintaining aspect ratio, with:

struct ContentView : View {
    var body: some View {
        GeometryReader { g in
            ZStack {
                Image("barbarian")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(maxWidth: g.size.width / 2,
                           maxHeight: g.size.height / 2)
            }
            .frame(width: g.size.width, height: g.size.height)
        }
    }
}

The GeometryReader acts exactly like the infinite frame we saw in flexible frames, it proposes the size of its parent to its child, but it also passes that proposed size g to our view builder.

And just like the infinite frame, the geometry reader doesn’t use the size of the child when deciding its own size; instead it always returns the proposed size from the parent as its own size.

An important gotcha is that within the view builder we need to do our own sizing, positioning and alignment; ZStack is perfect for this. We still need to position that, so we place it inside a frame that has the same size as the reader parent, and let the stack be centered inside it.

Finally inside the stack we place our image, and place that inside a frame that constrains its width and height to half the size of the reader.

image in geometryreader

In Secondary Views

With access to the proposed size of the parent, GeometryReader can seem powerful, but the resulting fixed size equally that can limit their usefulness. When combined with secondary views they become even more convenient.

As we saw above, when free floating, a geometry reader expands to fill the size proposed by the parent.

But because the proposed parent size of a secondary view is the fixed size decided by the view its attached to, that is the proposed size. Thus GeometryReader inside a secondary view returns the size of the view it’s attached to.

GeometryReader inside a secondary view returns the size of the view it’s attached to

For clarity I like to separate out complex secondary views into a separate property:

struct OverlaiedImage : View {
    var body: some View {
        Image("barbarian")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .overlay(overlay)
    }

    var overlay: some View {
        GeometryReader { g in
            Image("overlay")
                .resizable()
                .frame(width: g.size.width, height: g.size.height / 2)
                .position(y: g.size.height / 2)
        }
    }
}

In this example the Image in the body is allowed to be resizable while maintaining its own aspect ratio, with the ultimate bounds of that determined by whatever uses our OverlaidImage custom view.

We then use an .overlay secondary view to draw another image over the bottom half of that image.

In order to constrain that to the bottom half we need to know the size of the Image we’re drawing over, and the GeometryReader works for this because it’s in the secondary view.


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

Secondary Views in Practice

A secondary view, be it background or overlay, can be any view. We know from flexible frames that we can create views of fixed sizes, sizes based on their children, or sizes based on their parent. And we saw above that the proposed size of a secondary view is the fixed size of a parent.

So let’s put all this together, and build something cool! A hit points bar for our character that shows how much damage they’ve taken.

We ideally want the size of the hit points bar to be flexible to our needs, as we’ll use it in a few different places. For the character list, something like the following code would be ideal:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("rogue")
            VStack(alignment: .leading) {
                Text("Hasty River")
                    .font(.title)
                HitPointBar(hitPoints: 60, damageTaken: 27)
                    .font(.caption)
                    .frame(width: 200)
            }
        }
    }
}

First let’s start by having some text say how many hit points they have left, we’ll want this somewhere in the bar, so it’s a good point to start:

struct HitPointBar : View {
    var hitPoints: Int
    var damageTaken: Int

    var body: some View {
        Text("\(hitPoints-damageTaken)/\(hitPoints)")
    }
}

That’s actually already enough to get started, the Text has a fixed size and the HitPointBar view is layout-neutral, so the .frame we supply above sets the width. While the Text has no font size specified of its own, it inherits it from the environment so we can set it on the HitPointBar; the .frame is layout-neutral in height, so inherits the height from the caption-sized Text:

hit point bar with just text

I added the border to the view to verify the frame, but that’s already not bad. However the .border was on the .frame outside the view, if we look inside we see that the Text has its usual fixed size and is just positioned within it.

We still need the HitPointBar itself to fill this. In flexible frames I introduced infinite frames as frames that fill their parent, so we can use one of those:

struct HitPointBar : View {
    var hitPoints: Int
    var damageTaken: Int

    var body: some View {
        Text("\(hitPoints-damageTaken)/\(hitPoints)")
            .frame(minWidth: 0, maxWidth: .infinity)
    }
}

We make the frame of the text have the size of the parent in width (which we then fix in the ContentView), while still allow it to be layout neutral in height.

The result looks the same:

hit point bar with text and frame

But this time I’m able to place the .border around the .frame inside the HitPointBar. The text is positioned within that frame, and this frame can be the foundation of the rest of the view.

Once you learn to rely on the fixed sizes of views, and layout-neutral behavior of combinations of views, it’s actually easy to create flexible custom views by using the layout system rather than fighting it.

Okay so let’s add a secondary view to make the bar. Nothing says damage and hit points like a red lozenge:

struct HitPointBar : View {
    var hitPoints: Int
    var damageTaken: Int

    var body: some View {
        Text("\(hitPoints-damageTaken)/\(hitPoints)")
            .foregroundColor(Color.white)
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.red)
            .cornerRadius(8)
    }
}

Pay attention to the ordering of things, and remember that .frame creates a new view around the Text inside it. We deliberately attach the .background secondary view to this frame, which is taking its width from its parent and its height from its children views.

We then apply a .cornerRadius to the combination of the frame and secondary view, which encases them both in a clipping view that masks the boundaries. This means it’ll apply to the Text, background color, and anything else we added.

We also set the foreground color of the Text to white for better contrast.

hit point bar with text and background

Looking good, but we want that hit point bar to be filled with green if they’ve taken no damage, filled green from the left and red from the right according to how many hit points they have left.

That wouldn’t be too hard if the size of the view was fixed, but we’ve deliberately decided to make it flexible and up to the parent. Worse, we’ve decided that the height is going to be dictated by dynamic type, so is flexible as well.

There’s a tool for this, the geometry reader, and while it always expands to fill its entire parent, when we use it as a secondary view, the parent is the view it’s attached to.

For clarity I like to separate out complex secondary views into a separate property, so we first rewrite our control to do this:

struct HitPointBar : View {
    var hitPoints: Int
    var damageTaken: Int

    var body: some View {
        Text("\(hitPoints-damageTaken)/\(hitPoints)")
            .foregroundColor(Color.white)
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(background)
            .cornerRadius(8)
    }

    var background: some View {
        Color.red
    }
}

Now we know we’re going to want two things in this geometry view, a green color from the left for the hit points remaining, and a red color from the right for the damage taken.

There’s a few different ways to achieve that, and they’re all equally valid. For this example we’ll have the red color fill the entire view, and place a green color in front of it, so we’re going to need a z-axis stack for that with a leading alignment.

This all then goes inside the GeometryReader:

var background: some View {
    GeometryReader { g in
        ZStack(alignment: .leading) {
            Rectangle()
                .fill(Color.red)
            Rectangle()
                .fill(Color.green)
                .frame(width: g.size.width
                    * CGFloat(self.hitPoints - self.damageTaken)
                    / CGFloat(self.hitPoints))
            }
        }
    }

In general terms this view looks like something you’ve written before.

The GeometryReader expands to fill the proposed size given by the parent, except this time that proposed size is the size of the frame we put around the text, based on the text size.

A ZStack containing two views with leading alignment is nothing special, the first is a Rectangle filled with red—switching from just using the color for maximum readability, and the second is also a Rectangle just filled with the green instead.

The second Rectangle is constrained in size by placing it inside a frame, layout neutral in height, but fixed in width to that derived from the percentage of hit points remaining and the width returned by the GeometryReader.

The result is the flexible hit point bar we wanted:

hit point bar with text and progress

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

Padding

In views have fixed sizes, we introduced the idea that all views in SwiftUI are fixed in size, for example a Text view has the size required to render the string provided:

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

Creates a view with the exact bounds necessary:

text view

We also showed that the .frame modifier actually creates a new view with the dimensions specifies, and positions the Text view within it, such that:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .frame(width: 200, height: 200)
    }
}

Actually creates two views, a .frame that is 200×200 in size, and a Text within it with the exact bounds necessary to render its contents:

text inside frame

We looked into this process further in flexible frames, introduced the concept of layout neutral views that report their own size based on their children, and showed that in either dimension .frame can have a fixed size, be layout neutral, or through minimum and maximum size constraints base its own size on that proposed by its own parent.

We’ll now take a look at another useful modifier view, one that adds padding around its child view, and has a number of different forms that we can use:

func padding(_ length: CGFloat) -> some View
func padding(_ insets: EdgeInsets) -> some View
func padding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View

The first form sets the padding of all edges to the length specified.

The second form sets the padding of each of the edges to the specific individual values you specify through the EdgeInsets value.

The third form sets the padding of the set of edges you specify to the length supplied, leaving other edges unpadded. The third form also allows you to specify nil as the length, instead of zero, which instructs SwiftUI to use a system default amount of padding appropriate for the situation.

Default values for all parameters of the third form are provided, which uses the system default padding for all edges, we’ll use that in our example:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .padding()
    }
}

The .padding modifier is no different from modifiers like .frame, it doesn’t modify the Text in any way, it instead creates a new view that adds padding, and positions the Text view inside it as a child:

text with padding

The layout process is actually a little more interesting than just adding padding, and is almost but not quite layout neutral. It in fact works something like we see for stacks when considering spacing.

  1. Parent proposes a size to the padding view.
  2. Padding view subtracts the appropriate padding length from each edge.
  3. Padding view proposes this smaller size to its child.
  4. Child decides on its size.
  5. Padding view takes the child’s size, adds the appropriate padding length back to each edge, and sets that to its own size.
  6. Parent positions the padding view within its bounds.

Thus a .padding view always tightly wraps its child (aside from the padding itself), with both being positioned by the parent frame, but at the same time adds an additional constraint (the padding) to the size the child can be.

We can demonstrate this by placing the .padding inside a frame:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .padding()
            .frame(width: 200, height: 200)
    }
}

The .frame has a fixed size of 200×200, the .padding view subtracts the system default padding of 16px (in this case) from each side, and supplies the Text with a proposed size of 168×168.

That’s too small for Text to layout on one line, but still enough room to wrap over two lines, so it returns its size appropriately to do that. .padding adds back the padding before returning its size, and the .frame positions the padding view inside it.

text with padding inside frame

As we can see, the padding view still tightly wraps the Text, it isn’t increased in height or width to try and fill the parent frame, and is centered within it instead.

Exploding Stacks

A common layout desire is to place views in the corner of a larger view, or of the device. This is particularly interesting because it’s more instructive to visit the methods that don’t work and explain why, before showing the best way to do it.

Our desired result is as follows:

view with images in each corner

We’ll concentrate first on the problem of putting the dragoon Image in the bottom-right, since once that’s solved, the others are easy.

Having read about flexible frames and infinite frames, stacks, and alignments, our first attempt might be to use an alignment of a z-axis stack, and place that in an infinite frame:

struct ContentView: View {
    var body: some View {
        // ⚠️ This is an example that does not work.
        ZStack(alignment: .bottomTrailing) {
            Image("dragoon")
        }
        .frame(minWidth: 0, maxWidth: .infinity,
               minHeight: 0, maxHeight: .infinity)
    }
}

But that means we skipped views have fixed sizes, where we learned that .frame creates a new view and positions the modified view inside it. And that’s what happens:

view with frame around stack

The Image has a fixed size, and the ZStack has only the minimum size necessary to contain its children. The frame around it has the full size and positions the ZStack inside it, centered because that’s the default of the frame.

So since we can’t use a .frame around the ZStack to change the size of it, what can we do?

We can put the infinite frame inside the ZStack, around the Image:

struct ContentView: View {
    var body: some View {
        // ⚠️ This is an example that does not work.
        ZStack(alignment: .bottomTrailing) {
            Image("dragoon")
                .frame(minWidth: 0, maxWidth: .infinity,
                       minHeight: 0, maxHeight: .infinity)
        }
    }
}

I refer to this trick as exploding the stack, the stack has the minimum size of its children, but its child is the frame around the Image and that causes it to be sized as large as it can be:

view with frame around image inside stack

The ZStack now fills the entire device, and the Image is still fixed in size within it, but we’re still having an issue with the Image being centered rather than aligned.

This might seem like a surprise since the ZStack has a specified alignment of .bottomTrailing, but what we’ve missed here is that the child of stack is now the frame causing it to explode out; the Image is being positioned by the .frame and not the ZStack.

What we need to do is move the alignment to the .frame:

struct ContentView: View {
    var body: some View {
        ZStack {
            Image("dragoon")
                .frame(minWidth: 0, maxWidth: .infinity,
                       minHeight: 0, maxHeight: .infinity,
                       alignment: .bottomTrailing)
        }
    }
}

Since the ZStack has just one child, its alignment isn’t important, in fact the stack is not even necessary at all, but we’ll keep it in since we’re just aligning one view out of four.

view with alignment on frame

This does exactly what we want.

Now we can bring back the other three Image views, using the ZStack to overlay them together, but specifying different alignment for each:

struct ContentView: View {
    var body: some View {
        ZStack {
            Image("brawler")
                .frame(minWidth: 0, maxWidth: .infinity,
                       minHeight: 0, maxHeight: .infinity,
                       alignment: .topLeading)
            Image("rogue")
                .frame(minWidth: 0, maxWidth: .infinity,
                       minHeight: 0, maxHeight: .infinity,
                       alignment: .topTrailing)
            Image("barbarian")
                .frame(minWidth: 0, maxWidth: .infinity,
                       minHeight: 0, maxHeight: .infinity,
                       alignment: .bottomLeading)
            Image("dragoon")
                .frame(minWidth: 0, maxWidth: .infinity,
                       minHeight: 0, maxHeight: .infinity,
                       alignment: .bottomTrailing)
        }
    }
}

Each Image has its fixed size, and each is surrounded by a .frame that explodes out the surrounding ZStack to the proposed size of its own parent. Alignment is specified for each frame individually, to align the Image within it:

view with four images and alignment on frames

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

Secondary Views

Secondary views are one of the more interesting layout tools available in SwiftUI, to understand them first we have to recall that views have fixed sizes. To recap the process:

  1. Parent proposes a size to its child.
  2. Child decides on its size.
  3. Parent positions the child within its bounds.

Secondary views are useful because of where they fit in to this process, and how they interact with it. To demonstrate, let’s use a simple example:

struct ContentView : View {
    var body: some View {
        Text("Hasty River")
            .font(.title)
            .background(Color.yellow)
    }
}

We create a Text which will have a fixed size of its content, and then we add a secondary view using .background; the value of this is the secondary view added, in this case, a Color.

Color when used as a View simply sets its size to the proposed size received from its parent, fillings its bounds.

text with a yellow background

The result shows us that the proposed size for the secondary view is the fixed size decided on by the view its attached to.

the proposed size for the secondary view is the fixed size decided on by the view its attached to

So we can refine our process a little:

  1. Parent proposes a size to its child.
  2. Child decides on its size.
  3. Child proposes its size to its secondary view(s).
  4. Secondary view decides on its size.
  5. Parent positions the child within its bounds.

In our first experiment we just filled the secondary view with a color, what if we use a view there that’s inflexible about its size, and ends up being larger than the child? Perhaps an Image:

struct ContentView : View {
    var body: some View {
        Text("Hasty River")
            .font(.title)
            .background(Image("rogue"))
    }
}

If we’ve been paying attention we’re almost certainly going to expect that to break out of its bounds, but how does that affect the frame of the child its attached to?

text with background image overflowing bounds

The answer is that it doesn’t, the frame of the Text in green remains unaffected by the frame of the secondary view in red. All that happens is that the child positions its secondary view, even though it overflowed.

So now our process looks like:

  1. Parent proposes a size to its child.
  2. Child decides on its size.
  3. Child proposes its size to its secondary view(s).
  4. Secondary view(s) decides on their size.
  5. Child positions its secondary view(s) within its bounds.
  6. Parent positions the child within its bounds.

To see how this interacts with other views, let’s do a side-experiment using a VStack and some other lines of text:

struct ContentView : View {
    var body: some View {
        VStack {
            Text("My Character")
                .font(.caption)
            Text("Hasty River")
                .font(.title)
                .background(Image("rogue"))
            Text("Rogue")
        }
    }
}

If the secondary view has any part to play in the layout, we would expect to see the vertical stack account for it:

vstack of text with a background image on the middle text

The vertical stack ignored the secondary view completely; indeed everything we’ve learned about stacks should mean this isn’t a surprise.

We saw above that the Text did not change its size to account for the overflowing secondary view, so there was no way for the stack to account for it; after a view positions its secondary views they are otherwise completely removed from the layout process.

after a view positions its secondary views they are otherwise completely removed from the layout process

So a secondary view gives us two things:

  • a view that has a proposed size that is the decided size of the view it is attached to.
  • a view that is otherwise removed from the layout process.

The latter has the most utility in creating background views using .background, or overlay views using .overlay, that might be larger than their parent.

The former though can be extraordinarily useful in custom controls, we’ll look at making one in secondary views in practice.


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

Combining Stacks

In the post about stacks we looked at the individual tools of horizontal, vertical, and z-axis stacks. When used on their own they’re useful ways of laying out views along a single axis. But when used together they’re a powerful way of creating even the most complicated layouts.

Let’s continue our example of a character card, and see how we can combine stacks to make a more interesting view that shows the image, name, and class as below:

combined stacks example

We can see that we’re going to need two Image views: one for the character, and one for the class icon; and we’re also going to need two Text views: one for the name, and one for the class.

But how many stacks are we going to need, and of what types?

A simple approach I like to use to figure out the answer is to draw lines from edge to edge across the layout where the dividers between views are:

combined stacks annoyed with lines

What we’re looking for is a line that runs from one side of the layout to the other without crossing through any other view, this gives us our first stack division.

It’s not #2 because that passes through the character picture, and it’s not #3 because that passes through the character name. The only line that doesn’t pass through any view is #1, this vertically divides the space between the character picture on the left, and the details on the right.

Because it’s a vertical line, that means we want a horizontal stack.

struct ContentView : View {
    var body: some View {
        HStack {
            // TODO
        }
    }
}

To the left of it is only the character picture, we can disregard line #2 because that goes under the text that’s on the right hand side.

Since we don’t need to divide up the left side any further, let’s go ahead and put the Image in:

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

On the right hand side we still have lines #2 and #3 to deal with. We might be tempted to continue the horizontal stack for the class icon, but as we can see the line that divides from the class name still passes through the character name, so we can’t do that yet.

But line #2 now cleanly divides the remainder on the right of line #1, and since that’s a horizontal line, this means we want a vertical stack:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")
            VStack {
                // TODO
            }
        }
    }
}

Above this line we have just the character name, so we can put that in:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")
            VStack {
                Text("Nogistune Takeshi")
                    .font(.title)
                // TODO
            }
        }
    }
}

Below the line we still have two views, separated cleanly by line #3. Vertical line means a horizontal stack, and since we know what we’re doing, we can go ahead and just put those views in it:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")
            VStack {
                Text("Nogistune Takeshi")
                    .font(.title)
                HStack {
                    Image("class_icon")
                    Text("Barbarian")
                }
            }
        }
    }
}

Note that we not only have a combination of a horizontal and vertical stack, but the vertical stack embedded in the horizontal stack also has another independent horizontal stack within it.

This kind of arbitrary nestability of the stacks is part of their power.

So let’s see how this looks:

combined stacks with default alignments

Alignments

That’s pretty close, but it’s still not exactly what we were going for. Everything has been laid out in the right rough general area, but SwiftUI’s preference for center alignment has taken over.

Fortunately we can change that, and if you haven’t reviewed my post on alignments now is a good time to do so.

Recall that each stack has its own alignment for the children positioned within it, and that the alignment of a stack is perpendicular to the direction of its layout.

When combining stacks, recall from the post about stacks that a stack acts as a frame tightly wrapped around its children. So it participates in the layout process of its parent accordingly.

Let’s look in a little more detail at the above by showing the borders of all of the individual stacks:

combined stacks with default alignments and all stack borders

This makes it clearer that the horizontal stack around the class icon and name has been centered within the vertical stack shared with the character name.

So if we want to leading align those views together, it’s the alignment of the vertical stack we change.

The vertical stack has also placed spacing between them, and we can remove that at the same time:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")
            VStack(alignment: .leading, spacing: 0) {
                Text("Nogistune Takeshi")
                    .font(.title)
                HStack {
                    Image("class_icon")
                    Text("Barbarian")
                }
            }
        }
    }
}

By carefully considering the layout and desired effect, we only needed to make one small change:

combined stacks with leading stack alignment

Custom Alignments across Stacks

The default centering behavior for everything else seems to work pretty well, but after a while we get a bug that on smaller devices, the text wraps:

combined stacks with wrapped text

While this looks fine to us, the feedback from users, or designers, is that the class moving up and down relative to the picture is undesired. Instead they want the character name to grow upwards away from the class.

This is a good use for a custom alignment, which I cover in a bit more detail in the alignments post. After reading that, we might guess that we want the .center of the character picture Image to be aligned with the .lastTextBaseline of the Text for the character name.

Since these are vertical alignments, that means we’d apply the custom alignment to the HStack:

extension VerticalAlignment {
    enum Custom: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat { d[.top] }
    }

    static let custom = VerticalAlignment(Custom.self)
}

struct ContentView : View {
    var body: some View {
        HStack(alignment: .custom) {
            Image("barbarian")
                .alignmentGuide(.custom) { d in d[VerticalAlignment.center] }
            VStack(alignment: .leading, spacing: 0) {
                Text("Nogistune Takeshi")
                    .font(.title)
                    .alignmentGuide(.custom) { d in d[.lastTextBaseline] }
                HStack {
                    Image("class_icon")
                    Text("Barbarian")
                }
            }
        }
    }
}

But the Text we want to align is within a VStack, how do we handle that? Fortunately we don’t have to:

combined stacks with wrapped text and custom alignment

Because the Text is the only view within the VStack that defines a value for the .custom alignment we created, the VStack itself has the same value for the alignment.

This allows it to be aligned with its sibling Image according to an alignment guide set by one of its children.

Remember that a vertical alignment is always used by a horizontal stack, and a horizontal alignment used by a vertical stack. But a horizontal stack can (and will) apply a vertical alignment to a vertical stack child, and vice-versa.


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

Flexible Frames

In my first post I showed that in SwiftUI views have fixed sizes derived from the size of their content, but that that they can be flexible about what that fixed size is based on the size of their parent.

The layout process looks like:

  1. Parent proposes a size to its child.
  2. Child decides on its size.
  3. Parent positions the child within its bounds.

I also showed that while you cannot change the size of a view, you can use this process to influence the result by using frames. A frame is added to a view using the .frame view modifier, creates a new view with the size given, and then places the view inside it.

It’s important to remember that the modifier does not change the frame of the view being modified in anyway. We can demonstrate this using inflexible views such as Image; firstly by placing one in a frame that’s too big:

image in too large frame

Which shows that while the frame proposes a larger size to its child, the child still decides on its size based on the source, and all that’s left to do for the frame is position it.

And likewise if we place the Image in a frame that’s too small, it does not respect the bounds:

image in too small frame

This latter has a particularly interesting behavior, since the parent of the frame only receives the size of the frame for layout purposes, from the point of view of its siblings and all other views, the size of the Image is irrelevant.

Layout Neutral Views

So far in all our frame examples we’ve passed a fixed value to the width and height parameters, but if you take a look at the API you’d see that these are both optional.

func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View

What happens if we omit one? For example, the height:

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .resizable()
            .frame(width: 50)
    }
}

Note first that we made our Image resizable, which we should recall has it ignore the size of its source content, and instead return the size proposed by the parent.

We’ve placed the Image inside a .frame which will be its parent, and we’ve proposed a width for that parent, but we haven’t proposed a height:

image in frame with width only

What we see is that the image has the width we specified, taking the proposed width from its parent frame, but is as high as there is space available in the device, apparently using the height of the frame’s own parent. Likewise we see that the frame for the image has that same height.

When a view simply passes the proposed size it receives from its own parent down to its children without modification, and then chooses as its own size the size that its own child chose, we say that the view is layout neutral. Conversely when a view is layout neutral, the size of the view is the same as the size of its child.

when a view is layout neutral, the size of the view is the same as the size of its child

We’ve been using layout neutral views all the time probably without realizing it. The Image returned in our example code above is the value of ContentView.body, but ContentView itself is also a view, and is the parent to its body. We’ve never had to concern ourselves with this because ContentView is layout neutral, so invisible to layout.

So our frame is fixed in width, but layout neutral in height.

Unsurprisingly it works the other way too, when a frame is fixed in height but layout neutral in width, the Image becomes as wide as is available:

Flexible Frames

Now that we’ve more fully explored what we can do with the .frame modifier that lets us specify fixed widths and heights, it’s time to look at the other .frame modifier available to us:

func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center) -> some View

There are a lot of options here, allowing us to constrain the width and height in three ways:

  • minimum length
  • ideal length
  • maximum length

When any option is nil the behavior is to be layout neutral for that characteristic, while remaining constrained by the others you provide.

when any option is nil the behavior is to be layout neutral for that characteristic

In order to fully understand these we have to remember that the frame is a view in its own right, and only influences the size of its child view through the size it proposes to that child, and is itself influenced only by the size proposed by its own parent.

We can expand the layout process to include all three views:

  1. Parent proposes a size to the frame.
  2. Frame applies constraints to that size.
  3. Frame proposes the constrained size to the child.
  4. Child decides on its size.
  5. Frame decides on its size based on the child, and its constraints.
  6. Frame positions the child within its bounds.
  7. Parent positions the frame within its bounds.

We’ll show three frames in our examples, with the gray border being a parent frame supplied, and we’ll concentrate on the width parameters to show behavior, with the height being understood as behaving equivalently.

Minimum Length

The minimum length characteristic first comes into play when the frame applies constraints to the size proposed by its parent. The child receives a proposed length that is the larger of the parent’s length, and the frame’s minimum length. This is easiest to demonstrate when the parent is too small:

struct ContentView : View {
    var body: some View {
        // parent frame width is 50
        Image("barbarian")
            .resizable()
            .frame(minWidth: 100)
    }
}

The frame receives a proposed width of 50 from the parent, but has a minimum width constraint of 100; thus the child image receives a proposed width of 100 from the frame. Both the image and the frame overflow the parent:

frame with minWidth larger than parent

The minimum length also applies to the frame deciding it’s sized based on that of the child. If the child decides on a length that is smaller than the frame’s minimum length, the frame’s length becomes its minimum length and the child is positioned inside it.

struct ContentView : View {
    var body: some View {
        // image width is 101
        Image("barbarian")
            .frame(minWidth: 200)
    }
}

The frame receives a proposed width from the parent, and applies the minimum width constraint of 200; which the child receives as its proposed size. The child image is not resizable, so decides on a width of 101; the frame still applies its own minimum width, so decides on 200, and centers the image within that space.

frame with minWidth smaller than child

Maximum Length

Like with the minimum length, the maximum length characteristic applies both to the size proposed by the parent, and the frame’s decision as to its own size after the child has decided.

For the maximum length, the child receives a proposed length that is the smaller of the parent’s length, and the frame’s maximum length. This is easiest to demonstrate when the parent is too large:

struct ContentView : View {
    var body: some View {
        // parent frame width is 200
        Image("barbarian")
            .resizable()
            .frame(maxWidth: 100)
    }
}

The frame receives a proposed width of 200 from the parent, but has a maximum width constraint of 100; thus the child image receives a proposed width of 100 from the frame. The parent centers the resulting frame within its bounds:

frame with maxWidth larger than child

The maximum length also applies to the frame deciding it’s sized based on that of the child. If the child decides on a length that is larger than the frame’s maximum length, the frame’s length is still constrained to its maximum length, so the child overflows it:

struct ContentView : View {
    var body: some View {
        // image width is 101
        Image("barbarian")
            .frame(maxWidth: 50)
    }
}

The frame receives a proposed width from the parent, and applies the maximum width constraint of 50; which the child receives as its proposed size. The child image is not resizable, so decides on a width of 101; the frame still applies its own maximum width, so decides on 50, so the image overflows:

frame width maxWidth smaller than child

Ideal Length

The ideal length is used within situations where the parent is unable to propose a size to the frame, for example within a ScrollView. It specifies the length to report to the parent, instead of using that reported by its child.

Image has ideal lengths of its source image, even when resizable, Text has ideal lengths based on the string. Setting your ideal lengths tends to be necessary when creating flexibly-sized shapes and other custom views.

Interaction with Stacks

In the post about stacks we said we’d revisit how stacks lay out their children, and now we have enough information to understand what happens when laying out a mix of children.

horizontal stack of three views

The stack receives a proposed size from its parent, and as we saw it first subtracts the necessary spacing between its children since it knows how many.

Now that we know that views have a minimum, maximum, and ideal size we can understand what happens next.

First the minimum size of each view is subtracted and allocated to each view, since the Image has that as a fixed value equal to its others, this allocates the full size the image needs and leaves the remainder to the text.

Next it takes into account the maximum size of each view, should the remainder divided equally be larger than the maximum size of any view, those views are then allocated their maximum size since it leaves more of the remainder for the rest.

Finally for any remaining views, the remainder is divided equally.

Layout Non-Neutral Frames

So far we’ve only needed to specify one of the minimum or maximum lengths individually, the default behavior of being layout neutral usually sufficing for the unspecified length.

We can specify a minimum and maximum constraint together as long as they are in ascending order, this not only constrains the frame size within the given bounds, but also stops the frame from being layout neutral in that dimension.

Put another way, setting both a minimum and maximum length for a dimension bases the frame size on its parent, not its child.

setting both a minimum and maximum length for a dimension bases the frame size on its parent, not its child

For example we can constrain a frame to between the size of its parent and the size of its non-resizable child:

struct ContentView : View {
    var body: some View {
        // image width is 101, parent frame width is 200
        Image("barbarian")
            .frame(minWidth: 50, maxWidth: 150)
    }
}

In this case since the parent width is 200, and that does not fall in the range of 50–150, the child receives a proposed width of 150 instead. Since the child image is not resizable it chooses a width of 101.

This is where things now differ; if we had specified nil for either the minimum or maximum width, the frame would be layout neutral and simply choose its size as equal to the size of the child, since that’s within the bounds of the constraint.

But by providing both, the frame is no longer layout neutral, and instead chooses its size based on the proposed size it received from its parent. Since the proposed width was 200, and the maximum width of the frame is 150, the frame’s width is 150, and the child is positioned inside it.

frame with minWidth and maxWidth

As we saw in earlier examples, had we specified both constraints separately, the frame’s width would have been the child’s of 101.

We can also see what happens if the range of the constraints include the size of the parent frame:

struct ContentView : View {
    var body: some View {
        // image width is 101, parent frame width is 200
        Image("barbarian")
            .frame(minWidth: 50, maxWidth: 250)
    }
}

In this case the parent width of 200 falls within the range 50–250, so the child receives a proposed width of 200. Again the child image is not resizable, so it chooses a width of 101.

Since both a minimum and maximum width are specified, the frame is not layout neutral, and chooses its proposed size based on the parent. As that’s within its constraints, it chooses 200 and positions the child inside it.

frame with minWidth and maxWidth expanding to parent

We should round out by considering the case where the parent length is smaller than the minimum length of the frame:

struct ContentView : View {
    var body: some View {
        // image width is 101, parent frame width is 200
        Image("barbarian")
            .frame(minWidth: 225, maxWidth: 500)
    }
}

All rules learned thus far are observed. The proposed size to the child becomes the frame’s minimum width and the child takes a smaller size. Since the frame is not layout neutral it uses its minimum width as its closest to that of its parent, positions its child within that, and overflows its parent’s bounds:

frame with minWidth and maxWidth overflowing parent

Infinite Frames

Since specifying both the minimum and maximum length makes the frame sized based on the parent, constrained by the range given, an interesting case occurs when you pass .infinity as the maximum length:

struct ContentView : View {
    var body: some View {
        // image width is 101, parent frame width is 200
        Image("barbarian")
            .frame(minWidth: 0, maxWidth: .infinity)
    }
}

We can work through this as we have other examples.

The parent proposes a width of 200 to the frame, which is within the frame’s constraints of 0–∞, so the frame proposes a width of 200 to the child. The child image is still not resizable, so chooses 101 as its width.

Because both a minimum and maximum width are specified, the frame is not layout neutral. Since the proposed width from the parent of 200 is greater than 0 and less than ∞, the frame’s width becomes 200 and it positions the child inside it.

infinite frame

In other words, when specified with a smaller minimum width, and a maximum width of .infinity, the frame has the size of its parent.

when specified with a smaller minimum width, and a maximum width of .infinity, the frame has the size of its parent

Both are required, because if we’d specified only the maximum width, the frame would have been layout neutral and chosen the size of its child rather its parent.

Infinite frames can be a convenient way to adjust the alignment of a view within the bounds of a parent of unknown alignment, for example:

struct ContentView : View {
    var body: some View {
        // always layout top-leading, in any frame
        Image("barbarian")
            .frame(minWidth: 0, maxWidth: .infinity,
                   minHeight: 0, maxHeight: .infinity,
                   alignment: .topLeading)
    }
}

Ordinarily as a construct this would be unnecessary since you could simply change the alignment of the surrounding frame, but it’s useful example of how the infinite frame works.

In both width and height the proposed length from the parent falls within the 0–∞ bounds, so the child receives the parent size as a proposed size. Since both minimum and maximum lengths are specified, the frame is not layout neutral, so chooses its size as the parent size not the child. Finally it positions the child, using the alignment specified when creating the frame:

infinite frame with alignment

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

Alignments

In this post I’ll discuss how alignments work in SwiftUI, building our understanding of how they function, and finishing by demonstrating how to create your own custom alignments for specific needs.

Let’s start by taking a simple view that lays out horizontally three images of different heights:

struct ContentView : View {
    var body: some View {
        HStack(alignment: .top) {
            Image("rogue")
            Image("dragoon")
            Image("brawler")
        }
    }
}

If you haven’t read my post about stacks, read that now, since there are two invariants about HStack that are worth ensuring we understand:

  • the width of the stack will be the sum of the widths of its children views, plus spacing
  • the height of the stack will be the height of its tallest child, plus positioning

This means that the only flexibility is in the vertical positioning of each child, and that’s where alignment comes in. Alignment positions views in the perpendicular axis of the stack: vertical alignment for a horizontal stack, and horizontal alignment for a vertical stack.

A common error is to seek to use alignment to change the position of a child view in the same axis as the stack. If we’ve understood the behavior of the stack, we’ll realize that the horizontal position of a child in a horizontal stack is simply determined by the widths of the previous children in that same stack. Since views have fixed sizes that we set, we don’t need alignment at all for that type of layout.

But in our example, it’s the vertical positioning of views laid out horizontally that we care about. We’ve used the .top alignment in the example, and as expected this aligns all of the views to the top of of the stack.

We know what we expected the result to be, and hopefully it’s not a surprise, but it’s worth stepping through the process of alignment with this simple example before we take on something more complex.

views aligned using .top

To show the positions and sizes of the views, I’ve used a red border for the images, a green border for the stack, and showed the position of alignment with a yellow line.

An alignment is a key to a set of values that a view can return, called alignment guides. Each of these guides is simply a value of length in the view’s own co-ordinate space, from its top-left corner. The default value of the .top alignment guide for a view is therefore simply 0.

The process of alignment within the stack is to convert those values from the child view’s co-ordinate space, into the stack’s, vertically repositioning either the new child or the stack’s existing children so that the position of the same alignment guide across all of its children views match.

The stack considers each child in turn. First the stack increases its own width by the width of the new child, plus spacing if required. This creates space on the trailing edge of the stack for the new child to be placed.

For vertical positioning the stack maintains and updates its own value for the alignment, in its own co-ordinate space, which begins as 0. The stack compares the relevant alignment guide of the new child with this value.

If the new child’s alignment guide value is greater, the vertical position of all existing children in the stack is increased by the difference in values, and the stack’s value of the alignment is updated from the new child’s alignment guide.

If the new child’s alignment guide value is lesser, the vertical position of the new child is set to the difference in value, and the stack’s value of the alignment remains the same.

Finally the stack adjusts its height to ensure all of its children are accommodated.

Since the stack’s alignment value begins as 0, and in the case of .top each child’s alignment guide value is 0, the different is always 0 so vertical positioning occurs, the stack’s alignment remains at 0, and each child’s resulting vertical position is 0.

Center Alignment

Since .top was the easy case as no vertical repositioning was necessary, now let’s instead consider the apparently more complex case of .center.

The code is fundamentally the same, all we change is the value of the alignment parameter to HStack:

struct ContentView : View {
    var body: some View {
        HStack(alignment: .center) {
            Image("rogue")
            Image("dragoon")
            Image("brawler")
        }
    }
}

The invariants for the stack remain the same, which means that all that can change in the layout is the vertical positioning of children. As we saw by stepping through the process, this is determined by the values for the alignment guides of those children.

It’s helpful at this point to have a reference to the values of the alignment guides for each of the images we’re using.

Image.top.bottom.center
rogue06432
dragoon09145
brawler07638

As we’d hopefully expected the value of the .top alignment guide is always 0 and the value of the .bottom alignment guide is simply the height of the image, since that’s the length in the view’s own co-ordinate space from the top-left corner to the bottom of the image.

The value of the .center alignment guide is derived from these two, and is the distance from the top-left to the position halfway between its .top and .bottom alignment guide. This is done automatically for us, but the code inside SwiftUI to do that is simple and worth a pause to consider:

// The complicated, but correct, definition.
d[.center] = d[.top] + (d[.bottom] - d[.top]) / 2

// If we assume .top is always zero.
d[.center] = d[.bottom] / 2

We don’t need to define this alignment ourselves, so let’s go back to our example above and see what happens when we use .center alignment:

views aligned using .center

The result is what we expected, but now that we’ve walked through the alignment process for .top, and seen the values of the alignment guides for .center, we can follow the same process and reason about what’s going on.

The stack begins with a zero size, and an alignment value of zero.

The rogue image is added as the first child, and the stack increases its width by the width of that image. Since the value of 32 for the image’s .center alignment is greater than the stack’s current value of 0, and there are no existing children to reposition, the image is positioned at 0 and the stack’s alignment value is simply set from the image and is now 32. Finally the stack adjusts its height to the height of that image

The dragoon image is added as the second child, which increases the stack’s width by both the width of that image and its own value for spacing. Next it compares the new image’s value of 45 for the .center alignment guide with its own current value of 32; since this is greater, the existing children are vertically repositioned by the difference, in this case: 13. The new image is positioned at 0, and the stack’s alignment value is set from the new image, which is now 45. Finally the new image’s height is 91 which is larger than the stack height of 64, so the stack increases its height to that of the new image.

The brawler image is added as the third and final child, again increasing the width of the stack by the image width plus spacing. The value of 38 for the new image’s .center alignment guide is compared to the stack’s current value or 45; since this is lesser, it’s the new image that is positioned vertically by the difference of 7. The vertical position of the existing children, and the stack’s alignment value, both remain unchanged. The height of this image is 76, which is less than the stack’s 91, so no change in the stack size occurs as there is enough room for the image.

So as we can see, despite being a more complicated result, the process of .center alignment is identical to that of .top alignment, just with different values for the relevant alignment guide.

Custom Alignments

Let’s take this further and see what it takes to create a completely custom alignment. Remember that an alignment only affects the position along the perpendicular axis of the stack.

For our example, we want to align our characters so that they appear to be standing on the same plane. We could try using the .bottom alignment but due to the differences in drawings, that doesn’t quite cut it.

views aligned using .bottom

This is an ideal situation for creating and using a custom alignment.

First let’s remember that an alignment isn’t anything special, it’s just a key to a set of values associated with a view. To create a custom alignment, we need to define a new key, and to use a custom alignment we need to provide a value for that key for views we wish to align with it.

Keys for vertical alignments, such as we use for a horizontal stack, are instances of the VerticalAlignment type. The .top, .center, and .bottom alignments we’ve used so far are static properties of the type that return an appropriate instance.

To define our own we add our own static property that returns an instance for our new alignment. To create the instance we need a type that confirms to the AlignmentID protocol, this acts as the identifier for the alignment, and provides a default value for the alignment guide of any view that doesn’t otherwise specify it.

Since we want to align our images based on their feet, we’ll call our new alignment .feet. As a default value the existing .bottom alignment works well enough.

extension VerticalAlignment {
    enum Feet: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat { d[.bottom] }
    }
    static let feet = VerticalAlignment(Feet.self)
}

We can now use `.feet` anywhere we would have used .bottom and it’ll have the same effect.

That’s not quite enough, we now need to use this alignment by changing the values of the .feet alignment guides for our images. Remember that these values are simply lengths, in the image’s own co-ordinate space, from its top-left corner; we don’t need anything complicated here, we can directly use the vertical pixel positions that we measured in Photoshop.

struct ContentView : View {
    var body: some View {
        HStack(alignment: .feet) {
            Image("rogue")
                .alignmentGuide(.feet) { _ in 61 }
            Image("dragoon")
                .alignmentGuide(.feet) { _ in 84 }
            Image("brawler")
                .alignmentGuide(.feet) { _ in 70 }
        }
    }
}

Compared to our previous examples, in this example we set alignment parameter on HStack to our custom .feet alignment, and for each of our images we added an .alignmentGuide(.feet) that returns a value for that alignment guide.

views aligned using .feet

No new magic is required to perform this layout. As the stack adds each of children, as before, it simply reads the alignment guide values we’ve provided to determine how to vertically position each child.

Adding the second image repositions the first by 23, since the value of 84 for its .feet alignment guide is greater than the 61 for the first.

When the third image is added, it’s vertically positioned at 14 rather than adjusting the existing images, since the value of 70 for its .feet alignment guide is lesser than the 84 of the second, which is the greatest seen so far.

Aside from the different values, this is the exact same process used for the .top and .center alignments we’ve seen so far.

Derived custom alignments

For our example the hardcoded pixel positions were sufficient, but the .alignmentGuide modifier closure receives a value of the ViewDimensions type.

We can use this to derive complicated alignment values in the same way that SwiftUI derives the value for .center from the .top and .bottom alignment guide.

The closure accepts a dictionary of the current set of alignment values, for this example we didn’t need that, but we can use that to make any combination of alignments we desire.

For example an alignment guide that is 75% of the distance between the .top and .bottom:

Image("cleric")
    .alignmentGuide(.custom) { d in d[.bottom] * 0.75 }

Or a vertical alignment guide that is the derived from the width of the image:

Image("wizard")
    .alignmentGuide(.square) { d in min(d.width, d.height) }

Keep in mind that while powerful, you’re still limited to only affecting the vertical position within a horizontal stack, and the horizontal position within a vertical stack.

Expanding a stack with custom alignments

So far our examples haven’t caused the tallest element to be repositioned, this was deliberate to allow the fundamentals of alignment to be understood, but it is possible.

As we know, when a new child is added with a value for the alignment guide that is less than the stack’s value, the new child is vertically positioned by the difference.

We’ve only considered alignment guide values within the bounds of the tallest image, what if the values are out of those bounds, or cause the tallest image to need to be vertically positioned?

If there is insufficient vertical space for the new child at this position, the stack is expanded in height to accommodate it.

Let’s demonstrate with some code that uses a custom alignment to align the three images in such a way that the tallest image needs to be vertically positioned:

extension VerticalAlignment {
    enum Custom: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat { d[VerticalAlignment.center] }
    }
    static let custom = VerticalAlignment(Custom.self)
} 

struct ContentView : View {
    var body: some View {
        HStack(alignment: .custom) {
            Image("rogue")
                .alignmentGuide(.custom) { d in d[.bottom] }
            Image("dragoon")
                .alignmentGuide(.custom) { d in d[.top] }
            Image("brawler")
                .alignmentGuide(.custom) { d in d[.center] }
        }
    }
}

The intent is that the bottom of the rogue is aligned with the top of the dragoon and the center of the brawler, and that’s what we get:

views aligned using .custom

This alignment meant that the horizontal stack had to be expanded vertically to accommodate the vertical position of the dragoon image, and as we can see from the green border, it was.

Let’s go back over the process once more to see that the process is the same, just with a more detailed explanation of one of the steps.

The stack begins with a zero size, and an alignment value of zero.

The rogue image is added as the first child. The value of our .custom alignment guide is that of the image’s .bottom alignment guide, which is 64; since this is greater than the stack’s current value of 0, and there are no existing children to reposition, the image is positioned at 0 and the stack’s alignment value is simply set from the image and is now 64. Finally the stack sets its height to the height of the new image.

The dragoon image is added as the second child. The value of the .custom alignment guide is that of the image’s .top, which is 0; that is less than the current alignment value for the stack so it’s the new image that is positioned vertically by the difference of 64. The vertical position of the existing child, and the stack’s alignment value, both remain unchanged. But now the stack needs to increase in height, not just to accommodate the 91 pixel tall image, but its position of 64 as well; the stack’s height becomes 155.

The brawler image is added as the third and final child. The value of the .custom alignment guide is that of the image’s .center, which is 38; this too is less than the current alignment value for the stack, so this new image positioned vertically by the difference of 26. The vertical position of the existing children, and the stack’s alignment value, both remain unchanged. The height of this image is 76, plus the position of 38, is still less than the stack’s height of 155, so no change in the stack size occurs as there is enough room for the image.

As you can see, the only real change we made to the description of the process was to consider both the height and vertical position of each child when expanding the stack, rather than just the height.

ZStack Alignment

This post has concentrated on alignment within a horizontal stack to demonstrate the fundamentals. Transitioning the concepts to a vertical stack simply replaces the vertical positioning of the horizontal stack to a horizontal position within the vertical stack. But what about the z-axis stack?

ZStack has slightly different invariants compared to the horizontal and vertical, review my post about stacks if these are a surprise:

  • the width of the z-axis stack will be the width of its widest child, plus positioning
  • the height of the z-axis stack will be the height of its tallest child, plus positioning

Children are overlaid over each other, and we have flexibility in both their horizontal and vertical positioning. Since we know we can also expand the size of the stack through positioning, we can expand the size of the z-axis stack through alignment in both axes.

For alignment a ZStack accepts an instance of an Alignment type, which is initialized with instances of HorizontalAlignment and VerticalAlignment for each axis. We can build on our knowledge of custom alignments to define our own .anchor alignments that we’ll use to layout our views:

extension HorizontalAlignment {
    enum Anchor: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat { d[HorizontalAlignment.center] }
    }
    static let anchor = HorizontalAlignment(Anchor.self)
} 

extension VerticalAlignment {
    enum Anchor: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat { d[VerticalAlignment.center] }
    }
    static let anchor = VerticalAlignment(Anchor.self)
} 

extension Alignment {
    static let anchor = Alignment(horizontal: .anchor, vertical: .anchor)
}

These default to the center, but allow us to move the anchor of any one of our views, in either dimension. So let’s take advantage of this by making a ZStack of our views with the rogue and brawler above the dragoon:

struct ContentView : View {
    var body: some View {
        ZStack(alignment: .anchor) {
            Image("rogue")
                .alignmentGuide(HorizontalAlignment.anchor) { d in d[.trailing] }
                .alignmentGuide(VerticalAlignment.anchor) { d in d[.bottom] }
            Image("dragoon")
                .alignmentGuide(VerticalAlignment.anchor) { d in d[.top] }
            Image("brawler")
                .alignmentGuide(HorizontalAlignment.anchor) { d in d[.leading] }
                .alignmentGuide(VerticalAlignment.anchor) { d in d[.bottom] }
        }
    }
}

The process of alignment of a z-axis stack is much the same as a horizontal, except it maintains two alignment values, and compares both. Ordinarily this results in the single same point on each view being aligned over top of each other, but through custom alignments, we can achieve custom layouts:

views aligned using .anchor

Alignment Across Views

The examples thus far have shown alignment within a single stack view, but for more complicated layouts, we frequently use a nested hierarchy or multiple stacks. How do we align views using custom alignments through such?

Fortunately after layout of a stack is complete, the stack itself now has a value for our custom alignment guide, the value is simply that it used for aligning its children.

This means that the HStack views in our .feet example can be placed inside any stack that is also aligned by .feet.

In fact, I made all of the output samples in this post by just adding .border to the views, and taking a screenshot of the Xcode Preview. The line was added by creating a 1px high Rectangle in a ZStack around the aligned HStack.

The ZStack was then aligned using a vertical alignment of .feet, resulting in the two views being aligned together correctly.

struct ContentView : View {
    var body: some View {
        ZStack(alignment: Alignment(horizontal: .center, vertical: .feet)) {
            HStack(alignment: .feet) {
                Image("rogue")
                    .border(Color.red)
                    .alignmentGuide(.feet) { _ in 61 }
                Image("dragoon"
                    .border(Color.red)
                    .alignmentGuide(.feet) { _ in 84 }
                Image("brawler")
                    .border(Color.red)
                    .alignmentGuide(.feet) { _ in 70  }
            }
            .border(Color.green)

            Rectangle()
                .fill(Color.yellow)
                .frame(width: 200, height: 1)
        }
    }
} 

Note that the Rectangle for the line doesn’t specify a value for the .feet alignment guide, so the default from the alignment definition will be used—in this case, .bottom.


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

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.