SwiftUI: Layout & Interfaces

Nov 18 2021 Swift 5.5, iOS 15, Xcode 13

Part 2: Aligning Views

12. Alignment Guides

Episode complete

Play next episode

Next
Save for later
About this episode
See versions

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 11. Challenge: Align Nested Stacks Next episode: 13. Custom Alignment

This course was originally recorded in 2019. It has been reviewed and all content and materials updated as of October 2021.

With your knowledge of how to nest Stacks, and alignment, you have the capability to solve a lot of layout challenges where not every view is aligned in exactly the same way as its neighbors.

But let’s take a step back, and look into exactly what’s going on, when lining up views inside of Stacks. With a better understanding of how that works, you’ll sometimes be able to avoid the complexity of having an unnecessarily large amount of Stacks.

And, by the end of this episode, you’ll be able to achieve layouts that would be impossible, using only the built-in alignment values you’ve learned about so far. Here’s what’s going on under the hood:

When you assign an alignment value to a Stack, you allow it to get a piece of information from each of its child views. That’s a point along the axis —perpendicular to the stack– in the space of the child.

I like to imagine this as a line segment running across a child view. These are called Alignment Guides.

A Stack always gets one Alignment Guide from each of its children. Normally, they’re calculated for you, just by using an Alignment value on the Stack itself.

For example, here’s the default case of “center” HorizontalAlignment for a VStack. For every view, SwiftUI calculates the halfway point across the horizontal axis.

Then, all the views are matched up, based on those “center” Alignment Guides.

Combined with stacking tightly in the vertical dimension, that provides a complete internal layout for the VStack. And the Stack itself is positioned according to its parent’s rules —(and if there isn’t a parent, it just gets centered)— and then you have a fully complete layout.

So if all of your stacked views can use the same built-in layout guide, you’ve already learned everything you need.

But if you want to use different alignment guides for some or all of the views in a Stack, you’re going to need the alignmentGuide View Modifier. And you’ll apply it to children of the Stack.

The modifier has two parameters. The first one is an alignment value. If that doesn’t match up with what the parent stack is using, then the modifier doesn’t do anything. But if those alignment values do match, then this computeValue closure runs, and what it returns is used as the view’s Alignment Guide.

The closure uses one parameter, of type ViewDimensions. As you might expect, it’s got a width property, and one for height. The view they give you information about is the one on which you’re using the modifier.

So, for example, a center Alignment Guide is half of width or height, depending on alignment axis.

But you shouldn’t calculate that yourself, because you can subscript ViewDimensions, to access any existing alignment guides. And to illustrate that, let’s open this episode’s project.

Here, we’ve got a small Image View, a Text View, and a larger Image View. They’re all using center vertical alignment guides, because they’re inside of an HStack, which has center alignment by default.

Let’s shake that up by adding an alignment guide to the big Image.

(Reminder: control-option-click to bring up an inspector.)

        .alignmentGuide(/*@START_MENU_TOKEN@*/ /*@PLACEHOLDER=Guide@*/.top/*@END_MENU_TOKEN@*/) { dimension in
          /*@START_MENU_TOKEN@*/ /*@PLACEHOLDER=Value@*/dimension[.top]/*@END_MENU_TOKEN@*/
      }

alignmentGuide has an overload for both alignment axes, so you’ll need to fully qualify the name of what you’re matching against:

.alignmentGuide(VerticalAlignment.center)

By default, Xcode filled in the name of the parameter as dimension, but it’s actually a View-dimen*sions*.

dimensions

Still, our closure is going to be really short so I’m just going to use the $0 shorthand.

.alignmentGuide(VerticalAlignment.center) { $0 }

If you want to align the image based on its bottom edge, you could use the height property…

.alignmentGuide(VerticalAlignment.center) { $0.height }

…but I think it’s clearer to use the subscript, which takes a VerticalAlignment. Then we can use bottom.

{ $0[.bottom] }

…or maybe, top.

$0[.top]

The other views are both using their center alignment guides to match up with whatever we specify here. But we could, say, copy this line of code to the small Image…

        .frame(width: 60)
        .alignmentGuide(VerticalAlignment.center) { $0[.top] }

      Text("Alignment == 😻!")

…change it to use its bottom edge for an alignment guide…

$0[.bottom]

…and now you’ve got sort of a staircase effect.

I think it generally makes sense to have at least one of the views use the default alignment guide, for a stack, but you can modify all of them if you want.

        .multilineTextAlignment(.center)
        .alignmentGuide(VerticalAlignment.center) { $0[.top] }

And because what you’re working with are just floating-point numbers, you can do any math you want with them. Like scaling…

.alignmentGuide(VerticalAlignment.center) { $0[.bottom] * 3.5 }

…or translation.

.alignmentGuide(VerticalAlignment.center) { $0[.top] - 200 }

Technically, you don’t even need to use the ViewDimensions parameter, if you can compute your guide value without it.

.alignmentGuide(VerticalAlignment.center) { _ in 300 }

But I generally recommend basing your calculations on subscripted values, for clarity.

In the vertical dimension, the bottom alignment guide will have a larger value than the one for top. The absolute value of the difference between the two is the ViewDimensions’ height.

Generally, top will be zero, and so bottom will be the same as height, but there’s normally no reason to rely on those assumptions.

What’s important to remember is that positive offsets to a view’s vertical alignment guide will move that view up, in relation to other views it’s horizontally stack with.

And it works in reverse, too. Move the alignment guide down, the view moves up. Move the alignment guide up, the view moves down. It just happens that positive values point down, and negative ones point up.

But in the horizontal axis, things get trickier. Because leading and trailing flip, based on locale. Unfortunately, localization is a big enough topic that we’ll need to hold off on it for another course.

But, let’s have a look at Horizontal Alignment Guides, where things are simple enough to not have to worry about that. Change the HStack to a VStack.

VStack {

Now, because a VStack doesn’t use VerticalAlignment, all of these guides are just being ignored. But let’s change one to use HorizontalAlignment.

.alignmentGuide(HorizontalAlignment.center) { $0[.bottom] * 3.5 }

Now, you’re adjusting the horizontal guide, but you’re basing it on a vertical guide value! Sometimes, the ability to do that can come in handy. But that’s rare. Instead, let’s line up the leading edge of the Image with the centers of the other views.

.alignmentGuide(HorizontalAlignment.center) { $0[.leading] }

But, even though that guide is acting in the right dimension, if we change the alignment of the Stack, say to leading

VStack(alignment: .leading) {

…then again, the guide isn’t used. It has to be an exact match. And because there won’t be any ambiguity, as VerticalAlignment doesn’t have a leading option, we can use the shorthand syntax to perform this match.

.alignmentGuide(.leading) { $0[.top] - 200 }

…with that, you could, for example, have the trailing edge of one view match up with the leading edges of the others.

.alignmentGuide(.leading) { $0[.trailing] }

But if you wanted to use the center, instead, you’d have to specify the axis, because, again, there’s a center for both Alignment Dimensions.

.alignmentGuide(.leading) { $0[HorizontalAlignment.center] }

And, just to reiterate for clarity, no, these other alignment guide modifiers are not doing anything anymore. So you could delete them.

        .frame(width: 60)

      Text("Alignment == 😻!")
        .multilineTextAlignment(.center)

      Image("Xcode Magic")