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