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.