Alignments

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

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

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

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

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

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

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

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

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

views aligned using .top

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

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

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

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

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

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

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

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

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

Center Alignment

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

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

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

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

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

Image.top.bottom.center
rogue06432
dragoon09145
brawler07638

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

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

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

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

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

views aligned using .center

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

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

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

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

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

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

Custom Alignments

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

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

views aligned using .bottom

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

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

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

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

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

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

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

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

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

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

views aligned using .feet

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

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

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

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

Derived custom alignments

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

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

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

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

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

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

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

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

Expanding a stack with custom alignments

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

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

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

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

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

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

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

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

views aligned using .custom

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

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

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

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

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

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

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

ZStack Alignment

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

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

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

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

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

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

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

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

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

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

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

views aligned using .anchor

Alignment Across Views

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

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

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

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

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

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

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

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


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