Size-Limiting Frames

Occasionally we come across a layout where we need to limit the bounds of a view in a particular dimension. For example we might have a view where we show the character portrait along with the title they might be introduced by at court.

This works fine for the brawler:

character portrait with short title

But for the dragoon who has a bit of a khaleesi complex, it gets a little bit unwieldy and we need to limit the number of titles we actually show:

character portrait with long title

One way we can do this is with a .lineLimit on the Text, but when combined with accessibility font sizes, that can still grow taller than we intend. Sometimes we need to constrain a height in terms of pixels.

We might try to use a .frame with maxHeight specified:

struct ContentView : View {
    @ObservedObject var character: Character

    var body: some View {
        // ⚠️ This is an example that does not work.
        HStack {
            Image(character.imageName)
            Text(character.title)
                .frame(maxHeight: 200)
        }
    }
}

This works great for the dragoon’s overly long title, allowing it to flow on as many lines as can fit in the space, and then truncating the rest:

character portrait with truncated title

But when we try it on the brawler, there’s a new problem:

character portrait with short title and too-large frame

We specified a maximum height for the frame intending to limit the height of the text, which it does, but the frame itself has chosen the height we specified as the maximum, rather than the height of the text within it.

If we review flexible frames we can understand why.

When we omit a constraint to .frame, the frame is layout-neutral for that constraint and chooses the size chosen by its child. But when we provide a constraint the frame is no longer layout-neutral for that constraint, and no longer considers the size chosen by the child.

The frame received a proposed size from its parent, and applied the maximum height constraint, proposing the maximum height to the Text child, which in the dragoon’s case resulted in the truncation of their overly long title.

In both cases the Text chose a size equal or smaller to that height. Since the frame was layout-neutral in terms of minimum height, this set the minimum height of the frame to the size of the child, but the maximum height was supplied by us. The size proposed by the frame’s parent was outside the range of these two heights, and was constrained by the frame, to its maximum height.

This wasn’t what we wanted, we wanted the frame to constrain the size of the child, but still be layout-neutral.

Fortunately there’s a solution for this, and it involves the third set of .frame parameters we didn’t consider yet, the ideal size. Since we didn’t specify any value for .idealHeight then the frame’s ideal height is layout-neutral, that is, the ideal height of the frame is the height of the child.

SwiftUI gives us modifiers that fix the size of a view to its ideal size:

/// Fixes this view at its ideal size.
func fixedSize() -> some View

/// Fixes the view at its ideal size in the specified dimensions.
func fixedSize(horizontal: Bool, vertical: Bool) -> View

As we’re dealing with multi-line text we don’t want the first variant since text always ideally wants to be rendered on just one line, but the second variant is perfect since it will allow us to fix just the height of the frame.

We want to fix the size of the .frame so the modifier goes after it, rather then before—which would fix the size of the Text:

struct ContentView : View {
    @ObservedObject var character: Character

    var body: some View {
        HStack {
            Image(character.imageName)
            Text(character.title)
                .frame(maxHeight: 200)
                .fixedSize(horizontal: false, vertical: true)
        }
    }
}

The dragoon’s long title renders exactly as before, constrained by the maximum height of the frame:

character portrait with truncated title and correct-sized frame

But now the frame around the brawler’s title is fixed in height to the frame’s ideal size, that of the brawler’s title, and does not expand the stack unnecessarily:

character portrait with short title and correct-sized frame

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

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.

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.

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)
            }
        }
    }
}

Getting Started

Our ideal code has the vertical size of the hit point bar being determined by a font size, and the horizontal size being as wide as possible, while allowing a frame to constrain it.

Since the size of the font is key, we’ll start by having a Text view with a label saying how many hit points the character has left:

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

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

That’s actually already enough to get started. Contrary to my usual examples I’ve explicitly added a yellow border to the text so that we can see what’s happening. We’ll also add a green border to the point we use the HotPointBar:

HitPointBar(hitPoints: 60, damageTaken: 27)
    .font(.caption)
    .frame(width: 200)
    .border(Color.green)

I recommend using modifiers like .border and .background to debug your custom views, they can be hugely insightful.

hit point bar with just text

The Text has no font size specified of its own, so will inherit it from the environment, meaning the .font applied to the HitPointBar itself will be used.

As we saw in flexible frames HitPointBar is a layout-neutral view, so will tightly wrap the Text within it; and since we didn’t specify a height for the .frame, the frame is layout-neutral in height as well.

Thus the height of the HitPointBar is exactly the height of the text in the given font size, which is exactly what we want.

The width though is not yet correct, since the Text only has the width necessary for its contents, and HitPointBar tightly wraps that, it’s only the .frame in the parent that is the full width, and that’s in the wrong place to be useful.

We still need the HitPointBar itself to fill this frame.

In flexible frames I introduced infinite frames as frames that fill their parent, so we can use one of those (and drop the border from the parent call site):

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

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

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)")
            .frame(minWidth: 0, maxWidth: .infinity)
            .foregroundColor(Color.white)
            .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 view, 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)")
            .frame(minWidth: 0, maxWidth: .infinity)
            .foregroundColor(Color.white)
            .background(HitPointBackground(hitPoints: hitPoints,
                                           damageTaken: damageTaken))
            .cornerRadius(8)
    }
}

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

    var body: 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:

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

    var body: 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 almost certainly 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.

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 view:

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

struct OverlayView : View {
    var body: 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.

In ZStacks

As we saw in stacks and secondary views, the z-axis stack has a useful property where it’s only the children with the highest layout priority that influence the size of the stack, and those with lower priorities receive that size as a proposed size.

This can be usefully combined with GeometryReader just as we can with a secondary view, and can often produce more readable results.

Consider the above example, reformulated using a ZStack with the GeometryReader given a lower layout priority:

struct OverlaidImage : View {
    var body: some View {
        ZStack {
            GeometryReader { g in
                Image("overlay")
                    .resizable()
                    .frame(width: g.size.width, height: g.size.height / 2)
                    .position(y: g.size.height / 2)
            }
            .layoutPriority(-1)

            Image("barbarian")
                .resizable()
                .aspectRatio(contentMode: .fit)
        }
    }
}

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 size chosen by the view its attached to.

the proposed size for the secondary view is the size chosen 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.

ZStack as a Secondary View

When we looked at stacks, we covered the basics of the z-axis stack and mentioned that the size of the stack is the union of the bounds of all its children with the highest layout priority.

By using sets of children with different layout priorities, we can replicate the implementation of .background and .overlay within a ZStack.

For example, our initial example with a colored background:

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

Has an equivalent expression using a z-axis stack:

struct ContentView : View {
    var body: some View {
        ZStack {
            Color.yellow
                .layoutPriority(-1)

            Text("Hasty River")
                .font(.title)
    }
}

The ZStack first processes the Text child since it has the highest layout priority, and then chooses its own size as the same size, since there are no other children with that layout priority.

Next the Color receives as its proposed size the size of the stack, and since it’s completely flexible, occupies all of that space.

Note that the layout priority has no effect on the z-axis ordering of the children, and that the Color is still placed behind the Text.

.overlay can be replicated similarly, with the lower layout priority children being placed after those with a higher priority, so they appear on top.


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

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.

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, remember that a stack is tightly wrapped around its children and 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 choose their own sizes, derived from the size of their content, but that that they can be flexible about what size they choose based on the size proposed by their parent.

In the post about stacks we showed that the size chosen by a stack is derived from the sizes of its children, and reinforced why it’s important not to confuse the proposed size a child receives from its parent with the size later chosen by its parent.

The layout process looks like:

  1. Parent proposes a size to its child.
  2. Child chooses its own size.
  3. Parent chooses its own size based on the child, and its own constraints.
  4. Parent positions the child within its bounds.

I also showed that while you cannot override the chosen size of a view, you can use this process to influence the result by using views such as frames.

A frame is added to a view using the .frame view modifier; it creates a new view which both proposes the size given to its children, and chooses the size given as its own size, finally positioning the child view it was added to within its own bounds.

It’s important to remember that the modifier does not directly change the bounds 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:

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .frame(width: 200, height: 200)
    }
}

Which we should recall shows that while the frame proposes a larger size to its child, the child still chooses its size based on its source image, and all that’s left to do for the frame is position it:

image in too large frame

And likewise if we place the Image in a frame that’s too small:

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .frame(width: 100, height: 200)
    }
}

The Image‘s chosen size is not overridden by the frame, and exceeds its bounds:

image in too small frame

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

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 choose 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, using the height proposed by 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. Simply: 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 examples is the child of its frame, which is made to be the content of a view by returning it as the value of the computed ContentView.body property. But ContentView itself is also a view, and is the parent to its body, and child to its parent, the window.

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:

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

The Image becomes as wide as is proposed by the frame’s parent to the frame:

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 chooses its own size.
  5. Frame chooses its own size size based on the child, and its own 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 equivalent to the device in our other examples, 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 choosing it’s own size based on that of the child. If the child chooses 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 chooses a width of 101; the frame still applies its own minimum width, so chooses 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 choice as to its own size after the child has chosen.

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 choosing its own size, based on that of the child. If the child chooses a length that is larger than the frame’s maximum length, the frame’s length is still constrained to its maximum length, and 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 chooses a width of 101; the frame still applies its own maximum width, so chooses 50 for its own width, and 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, specifying the length to choose in that situation.

Image has ideal lengths of its source image, even when resizable, Text has ideal lengths based on its string content rendered on a single line.

Setting ideal lengths tends to be necessary when creating flexibly-sized shapes and other custom views, for example by using geometry reader, that are going to be used inside ScrollView.

The default values for the ideal length are nil which means that the ideal length will be layout neutral, and be the length of the child. We can use this combined with fixedSize to cause frame’s to hug their children while proposing larger possible sizes, see size-limiting frames for an example.

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 length 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 length on its parent, not its child.

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

For example we can constrain a frame to between the width of its parent and the width 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 proposed width from the frame’s parent 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 have had the potential of being layout neutral and able to choose its width as equal to the width of the child.

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

frame with minWidth and maxWidth

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 width based on the proposed width from its own parent. As that’s within its constraints, it chooses 200 and positions the child inside that:

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 width the child becomes the frame’s minimum width and the child chooses a smaller width. Since the frame is not layout neutral it chooses its minimum width as its own width, since that is closest to that of the proposed width from its parent.

Within its chosen width, it positions its child, 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 0 as the minimum length and .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 chooses its own width to be 200 and it positions the child inside that:

infinite frame

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

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

Both are required, because if we’d specified only the minimum or maximum width, the frame would have been potentially layout neutral and chosen the width of its child rather the proposed width from 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 of the frame receives the proposed size from the frame’s parent as a proposed size.

Since both minimum and maximum lengths are specified, the frame is not layout neutral, so chooses its size as the proposed size from its parent, not the size chosen by its 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")
        }
    }
}

After reading about stacks you’ll expect that each of the three Image child views will be laid out horizontally from the HStack‘s leading to trailing edge, with some spacing between them.

You’d know that the size of the HStack is the union of the bounds of its children, and that this means that its width is the sum of those plus spacing, and its height is that of its tallest child.

It was also explained that the alignment of each child can change the positioning and height of the stack, and that’s what we’ll look at now.

We’ll look at horizontal stacks first, and skip over vertical stacks since they function the same way just in a different axis, and then we’ll cover the z-axis stack.

Alignment Basics

Alignment positions views only 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, plus spacing.

Since views choose their sizes we don’t need alignment at all for that; we simply ensure the preceding views choose widths that we want, or wrap them in .frames that choose the desired width.

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:

views aligned using .top

To our usual red borders for the images, we’ve used a green border for the stack, and added a yellow line to indicate the common point of alignment for the views.

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.

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 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, comparing the relevant alignment guide of the new child with its own value, which begins as 0.

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, and the stack’s value of the alignment is updated to be equal to 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, and the stack’s value of the alignment remains the same.

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 no vertical repositioning 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")
        }
    }
}

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

By default, the .top alignment guide is always 0 and the value of the .bottom alignment guide is simply the height of the view, since that’s the length in the view’s own co-ordinate space from the top edge to the bottom.

The value of the .center alignment guide is derived from these two, and is the distance from the top 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

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 an alignment value of 0 and considers each child in turn.

The rogue Image is the first child, with a value of 32 for its .center alignment guide. 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 guide value set to 32.

The dragoon Image is the second child, with a value of 45 for its .center alignment guide. Since this too is greater than the stack’s current value of 32, the existing children are repositioned by the difference, in this case 13. The new Image is positioned at 0 and the stack’s alignment guide set to 45.

The brawler Image is the third and final child, with a value of 38 for its .center alignment guide. This is less than the stack’s current value of 45, so the new Image is positioned vertically by the difference of 7, and the stack’s alignment guide left unchanged.

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 considers each of children, as before, it simply reads the alignment guide values we’ve provided to determine how to vertically position each child.

For the second Image with a value of 84, this is greater than the stack’s alignment value of 61 (obtained from the first Image), so the existing children are repositioned by the difference of 23.

For the third Image with a value of 70, this is less than the stack’s alignment value of 84 (updated by the second Image), so this child is positioned at the difference of 14.

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 height of the view:

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

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

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 considered 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?

Since the size of the stack is the union of the bounds of its children, this includes any vertical repositioning, and thus the stack is expanded in height to accommodate them.

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:

The stack begins with an alignment value of zero and considers each child.

The rogue Image is the first child, with a .custom alignment guide value of 64. This is greater than the stack’s current value of 0, and there are no existing children to reposition. The child is positioned at 0 and the stack’s alignment value set to 64.

The dragoon image is the second child, with a .custom alignment guide value of the image’s .top, which is 0. This is less than the stack’s current value of 64, so it’s this child 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.

The brawler image is the third and final child, with a .custom alignment guide value of the image’s .center, which is 38. This too is less than the stack’s current value of 64, so it’s this child that is positioned vertically by the difference of 26. The vertical position of the existing children, and the stack’s alignment value, both remain unchanged.

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?

The size of a ZStack is still the union of the bounds of its children, just that its 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 vertical size of a horizontal stack through vertical alignment, it should come as no surprise that 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.

To illustrate we can just look at the code used to add the yellow line in the sample previews in this post. The HStack in the examples was contained within a ZStack, along with a 1px high Rectangle.

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.