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.

Stacks

The three most important layout tools in your SwiftUI toolbox are the three kinds of stacks: horizontal, vertical, and z-axis (yes, there really is no good adjective for this one).

These views are parents to multiple children, and in order to fully understand how these lay them out, you should first understand that views choose their own sizes.

Horizontal Stack

A horizontal stack is created using an HStack and a view builder that specifies its children:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")

            Text("Nogistune Takeshi")
                .font(.title)
        }
    }
}

It lays out its children sequentially from its leading edge to its trailing edge:

hstack

We can learn a lot by extending and constraining this simple example.

The first child is an Image which chooses its size to be the size of its source image. The second child is a Text which has chosen its size as that needed to render its string in the chosen font.

The Text is placed horizontally in the stack after the image, with space between them. We can specify the amount of space we want with the optional spacing parameter to the HStack, or we omit the parameter as we did here to allow the stack to choose the spacing.

When we allow the stack to choose the spacing, it does not choose a fixed value, but a value that makes sense for each specific pair of children.

Since the Text is not as tall as the Image, the stack vertically centers it. This is an entire topic in of itself, and you can read more in alignments. For now we’ll stick to the default centering behavior.

The width of the stack is the sum of the widths of its children, plus the spacing between them.

In the example above the stack has the height of the first child, the Image, which also happens to be the tallest. We can verify whether the height of the stack is that of the first child, or the tallest, by switching the order:

struct ContentView : View {
    var body: some View {
        HStack {
            Text("Nogistune Takeshi")
                .font(.title)

            Image("barbarian")
        }
    }
}

If the stack took the height from the first child, the Image would have had to overflowed the stack’s bounds; as we see, it does not:

hstack with the children in reverse order

This confirms that the height of the stack is the height of the tallest child. When we look at alignments we’ll learn that the height of the stack also includes the vertical positions of its children, thus a more general rule is that the stack’s bounds are the union of the bounds of its children.

the stack’s bounds are the union of the bounds of its children

We can confirm that the stack’s bounds are derived from the children by looking at what happens if the size of the device is larger than the combined sizes of the stack’s children:

hstack in larger frame

The stack does not expand to fill the size proposed by the device, its parent. The children of the stack chose their own sizes, and stacks are no different. Stacks choose their own sizes, derived from the sizes of their children.

Stacks when Constrained in Size

We know that views choosing their own sizes doesn’t mean that they can’t be flexible about the size that they choose, and we saw in that post that Text can return different sizes by truncating, tightening, and wrapping its string.

Let’s take a look at what happens when there is not enough space available for all of the children of the stack to be laid out horizontally.

We can do this by adding a .frame to the stack, which as we should recall, creates a new view with the stack as a child, and proposes the size given to it to the stack:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")

            Text("Nogistune Takeshi")
                .font(.title)
        }
        .frame(width: 280, height: 100)
    }
}

Since there is not enough space for both the Image and the Text to be laid out horizontally, the Text can help out by wrapping its contents, reducing its width by increasing its height:

hstack with wrapped text

But what if we had more than one Text in our stack, which one would adjust its size?

To see what happens, let’s divide our single Text into two, one each for the character’s family name and given name respectively. Since we want the Text to be truncated rather than wrapped, we’ll also throw in a .lineLimit:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")

            Text("Nogistune")
                .font(.title)
            Text("Takeshi")
                .font(.title)       
        }
        .lineLimit(1)
        .frame(width: 280, height: 100)
    }
}

Rather than allowing the first Text to take up most of the space, and the second to extra-truncated to fit, the stack instead divides the space up fairly between them and causes both to truncate:

hstack with two truncated txt

Yet while the space has been fairly divided between the Text children, the Image doesn’t seem to have been affected.

Proposed Sizes in Stacks

We gave a warning in views choose their own sizes to not confuse the proposed size a view receives from its parent with the size the parent chooses for its own, and now we can review why.

Like every view the stack receives a proposed size from its parent; in our examples, a frame or the device. And as we’ve already seen, the size of the stack is derived from the size of its children.

When the stack lays out its children, they receive a proposed size from the stack, but that proposed size cannot be the size of the stack since it has not been chosen yet. Neither is it the proposed size the frame received from its parent, which would not fairly distribute space as we’ve seen the stack does.

Instead the stack makes proposed size offers to its children in order of their increasing horizontal flexibility.

The stack begins with the proposed size it received from its own parent, and since it knows how many children it has and how much space to place between them, it can subtract the sum of the necessary spacing to obtain the space available for all of its children, which it can then divide equally

As the least flexible child, the Image receives the first offer of a proposed size. Since the Image is inflexible, it chooses a size without considering this. The stack subtracts the chosen size from the available space, and then divides the remainder equally again.

As the more flexible children, the two Text views next receive an offer of a proposed size, taking their equal share of the remainder and truncating equally.

In flexible frames we’ll learn how we can adjust a view’s flexibility by specifying minimum and maximum lengths.

Layout Priorities

If we want a little more control, for example if we want the character’s family name to be less likely to be truncated than their given name, stacks give us a way to control the order in which children are laid out.

By increasing the layout priority of certain children, the stack introduces an extra layout pass for those children before their siblings:

struct ContentView : View {
    var body: some View {
        HStack {
            Image("barbarian")

            Text("Nogistune")
                .font(.title)
                .layoutPriority(1)
            Text("Takeshi")
                .font(.title)       
        }
        .lineLimit(1)
        .frame(width: 280, height: 100)
    }
}

After subtracting spacing from the proposed size, the stack will consider children in order of decreasing layout priority. For those of the same layout priority, the stack as before will make offers in order of their increasing horizontal flexibility.

So in this example we’ve increased the priority of the Text containing the family name, ensuring that is laid out first, and gets the space it needs.

Next the two remaining children with a default zero layout priority are considered. Since the Image is the least flexible, that receives the first offer and chooses its size. Finally the Text containing the given name is proposed the remainder.

hstack with layout priority truncating second text only

Vertical Stack

The vertical stack is very similar to the horizontal, it is defined with VStack:

struct ContentView : View {
    var body: some View {
        VStack {
            Image("barbarian")

            Text("Nogistune Takeshi")
                .font(.title)
        }
    }
}

Rather than laying out its children horizontally, it lays out its children sequentially from its top edge to its bottom edge:

vstack

Just like the horizontal stack, each child view chooses its own size, and. the vertical stack itself chooses its own size, derived from the sizes of its children.

Vertical stacks obey all of the same rules and constraints as horizontal stacks, just in a perpendicular axis.

Since it’s arranging its children vertically, it’s the width of the vertical stack that is the width of its largest child. Smaller children are positioned horizontally within that space, with the default alignment to be centered.

Vertical stacks can be particularly useful to demonstrate the default spacing behavior, for example if we split the surname and family name as we did for a horizontal stack example above:

struct ContentView : View {
    var body: some View {
        VStack {
            Image("barbarian")

            Text("Nogistune")
                .font(.title)
            Text("Takeshi")
                .font(.title)
        }
    }
}

The result includes spacing between the Image and first Text child, but no spacing between the two adjacent Text children:

vstack with no spacing between text

Z-Axis Stack

The third kind of stack is neither horizontal or vertical, but instead overlays its children on top of each other. It is defined using ZStack:

struct ContentView : View {
    var body: some View {
        ZStack {
            Image("barbarian")

            Text("Nogistune Takeshi")
                .font(.title)
        }
    }
}

The children are overlaid sequentially, with the first defined on the bottom and the last defined on top:

stack

Just like the horizontal and vertical stacks, each child view chooses its own size, and the z-axis stack itself chooses its own size too, derived from the sizes of its children.

While it might seem limited in usefulness, the z-axis stack can actually be one of the most powerful tools for combining views, and is one of the fundamentals for creating new controls, or overlaying shapes.

In this simple example all of the children are laid over top of each other. We’ll learn when we look at alignments that we can move them apart from each other, and in exploding stacks how we can combine alignments and flexible frames inside a z-axis stack.

As with the horizontal and vertical stacks, the bounds of a z-axis stack are the union of the bounds of its children; but with an added twist: only the bounds of its children with the highest layout priority. We’ll look at this in more detail when we look at secondary views.

Combining Stacks

More complex layouts can be created by combining stacks together, a topic complex enough to deserve its own post.


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

Views Choose Their Own Sizes

One of the first things presented about SwiftUI is that its views choose their own sizes, and that once chosen, those choices cannot be overridden. This is such a simple statement that it would be easy to move on quickly to get to the good stuff.

This would be a mistake, because this simple statement fundamentally changes everything you know about layout. Failing to take the time to learn SwiftUI will leave you constantly feeling like your fighting to achieve even the simplest layouts. This would be a mistake as the design of SwiftUI has significant benefits over UIKit and AppKit, and it’s worth learning to think differently.

In this post we’ll look at two views that form the basic building blocks of many layouts, and what this assertion means for them: Image, and Text.

Images

Let’s start by placing an Image in our layout:

struct ContentView : View {
    var body: some View {
        Image("barbarian")
    }
}

The specific image we’ve used has a size of 101 × 92 pixels and the resulting view has the same size, centered on the screen of our device:

an image

The preview above is rendered using SwiftUI and includes additional borders to help visualize the layout. In this preview, and in future previews, a red border is used to show the bounds of images, and a gray border shows the bounds of the device. We’ll learn more about secondary views such as borders in other posts, but when they are only used to clarify the bounds of views in examples, we’ll omit them for clarity.

We can use the .frame modifier on the image to place it inside a frame:

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

Coming from UIKit or AppKit, this may not be what we expected:

an image inside a frame

Rather than stretching to fill the frame, the image has retained its chosen size and been centered within the frame’s bounds. In the preview above, and in future previews, we’ll use a green border to denote the bounds of frames.

It turns out that .frame does not modify the properties of the view it is applied to; it creates a new “frame” view with the specified size, and positions the view it’s applied to (the Image in this case) as a child inside it.

.frame does not modify the properties of the view it is applied to; it creates a new “frame” view with the specified size, and positions the view it’s applied to as a child inside it.

This is worth taking a moment to consider, because it’s a fundamental of SwiftUI. Modifiers like .frame create new parent views containing the view they are applied to as a child, and are extremely useful for building layouts of smaller views.

The size chosen by the Image view is based on the dimensions of the image and its properties in the asset catalog. This size cannot be overridden by its parent.

For an extreme demonstration of this, we can try and place the image inside a frame that’s too small for it:

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

We might have expected the Image to be resized, at least in this case even if not the other, but in fact it simply overflows the frame’s bounds:

an image overflowing a frame

We can use this behavior to create overlaid views, or we can customize it through clipping modifiers such as .clipped or .cornerRadius to constrain the drawing to the frame’s bounds. Since this post is about layout, we’ll cover those in future posts.

We’ll come back to images again, but we need to learn more first, so we’ll take a look at Text instead.

Text

Let’s replace the Image in our layout with some Text instead, perhaps the name of our barbarian character:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
    }
}
a text

In this preview, and in future previews, we’ve added a yellow border to show the bounds of text, and again used the gray border to denote the size of the device.

Just like Image before, the Text has been centered within the bounds of the device. Text too has a chosen its size, which is simply the size necessary to render the string given to it.

As with images, we can add a .frame to text views to build larger layouts:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .frame(width: 200, height: 200)
    }
}

Knowing that a .frame creates a new view that contains the Text, we we should not be surprised that the size of the Text remains the same as before, and that it’s simply centered within the frame just as it was previously centered within the device:

a text inside a frame

Text gives us a few different ways to influence the size it chooses, for example by specifying the .font that we want to use.

It’s our hero’s name, so let’s use a .title font:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
    }
}

As we would expect, since it’s now using a larger font, the view now has a larger size than it had before:

a text with a title font

So to complete the loop, let’s place our larger title-font Text inside a frame that would be too small for it:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .frame(width: 200, height: 200)
    }
}

We saw with similar code using Image that it kept its chosen size and overflowed the bounds of the .frame. But Text has already let us change its size through the .font modifier, so we might expect different behavior:

a multi-line text in a frame

Instead of overflowing the bounds of its parent frame, the Text was able to choose a different size that fit within it. In this case it wrapped its content, choosing a size lesser in width but greater in height.

This doesn’t contradict what we’ve just learned about chosen sizes not being able to be overriden, it actually demonstrates another key principle we haven’t learned yet: a view receives a proposed size from its parent before it chooses its own size.

a view receives a proposed size from its parent before it chooses its own size

It’s important not to confuse this proposed size a view receives from its parent with the size the parent chooses for itself. In most cases in SwiftUI a parent view chooses its size based on the size of its children. We’ll learn more about this later when we look at flexible frames.

Proposed Sizes

Understanding what happened in the previous example, and the process by which the views were sized, and positioned, is key to understanding the flexible nature of SwiftUI’s layouts.

The Text view chooses its own size, and once chosen that size cannot be overridden. But Text is also flexible as to what sizes it can choose, depending on the proposed size it receives from its parent; in this case the frame we added to it.

We’ve specified a size of 200 × 200 for the frame, so when the frame is ready to position the Text inside it, it proposes the same size to its child, and only then asks the child to choose a size. In the example without the frame, the Text received a proposed size of the bounds of the device itself.

In the example with the frame, the width proposed was insufficient to render the entire string, but there was sufficient height available to wrap the contents over two lines instead.

Once Text has determined the best way to layout its contents within the proposed size, it then chooses the size necessary to render its contents in that way.

The size chosen is still usually smaller than the proposed size, and always tightly hugs the content; it does not grow to fit. As you can see in the example above, it did not simply choose the proposed width as its own width, it chose the largest width necessary for the line-wrapped string, and it was still necessary for the frame to center the text within it.

Just as with Image being too large for the frame, a similar case occurs when a frame is added to a Text without enough height, for example:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .frame(width: 200, height: 10)
    }
}

Just as Image overflowed the proposed size from the frame when it was too small, so does Text:

text in a too-small frame

Text Truncation

Wrapping is not Text‘s only trick for choosing a different size, it can also tighten by compressing space between characters, and it can decrease its width and height by truncating its content, rendering a shortened version of the string with an ellipsis:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .lineLimit(1)
            .frame(width: 200, height: 200)
    }
}

Here we’ve added a .lineLimit modifier removing the default unlimited number of lines and replacing it with a new value of 1, this forces Text to use a different approach for choosing its size when the proposed size is not sufficient:

Since the Text wasn’t able to fit the full width of the string in the space proposed, and was not permitted to use multiple lines, the Text instead decreased its width by truncating its contents.

Note again that the width chosen by the Text is only enough to render the truncated line, and it is still centered within the frame.

The same truncation can occur in the situation where there isn’t sufficient proposed height for the text to expand to multiple lines, for example:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .frame(width: 200, height: 50)
    }
}

Even though no line limit is specified, the proposed height from the frame is only sufficient for a single line, so the content is truncated to fit the proposed space in this example too:

text truncated by limited height

Resizable Images

Now that we’ve learned that a view choosing its own size doesn’t mean the view can’t be flexible about what that chosen size is, we can revisit Image.

By default images have the size of their source, but they have the option to be resizable:

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

A resizable image chooses the same size proposed by its parent, entirely filling any available space:

a resizable image

By default the image will fill the space by stretching the source image, but by using .resizable(resizingMode: .tile) it would have repeated the source image:

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

Note that the size of the Image is still the proposed size of the frame, it’s simply rendering the source image multiple times within its bounds:

a tiled resizable image

In many cases neither of those behaviors are what we want, and instead we want the image to fill as much of the parent as possible, while retaining the aspect ratio of its source.

Fortunately there’s a modifier to specify that too called .aspectRatio.

But we can’t just specify that alone, we still need the .resizable modifier since we want to constrain the aspect ratio of the image after making it resizable.

Modifier Ordering

When specifying multiple modifiers, the ordering matters. Since each modifier applies to the view it’s added to, adding a corresponding superview to the view hierarchy, this means that in SwiftUI modifiers are written from the inside out.

in SwiftUI modifiers are written from the inside out

To make a resizable image constrained by the aspect ratio of the source, we need to specify both modifiers, as well as the frame:

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 200, height: 150)
    }
}
view hierarchy of an aspect ratio constrained resizable image with a frame

In this example code, the .resizable modifier is added to the Image view, which means that Image becomes a child of the .resizable modifier just as it was of .frame in our earlier examples.

Then when we wish to add a second .aspectRatio modifier, it is the resizable image that we wish to constrain the aspect ratio of, so we add this modifier to the end of that chain.

Finally it is the aspect-ratio constrained resizable image that we wish to place inside the .frame, so the frame comes last. It’s important to remember that while the .frame is listed last in the chain, it is the top-most parent, and that the Image while listed first, is the bottom-most child.

The results are what we intended:

a resizable image with an aspect ratio in a frame

Based on the size of its source, and of the proposed size from the parent, the Image has chosen to use the proposed height of the parent, scaling the source up to fit, and has chosen a width that retains the aspect ratio of the original source.

Since the Image therefore no longer utilizes the full size of its parent, the parent centers it.

With these rules firmly in our minds, we can now look at combining multiple views in our layouts by using stacks, or look at the interesting case of flexible frames.


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

Xcode: two Apps with a shared private Framework, in a Workspace

I’ve been working on a reasonably large iOS project that has ended up consisting of multiple, related, iOS apps for which I’ve wanted to share a large amount of code through the use of shared, private, frameworks. This seems like it should be simple, but instead I’ve run into all kinds of issues.

When building, you might end up with a warning about a missing directory:

ld: warning: directory not found for option '-F…/build/Debug-iphoneos'/

When trying to start the app in the iOS Simulator, without Xcode attached, it might crash with the error image not found:

Dyld Error Message:
  Library not loaded: @rpath/….framework/…
  Referenced from: …/Library/Developer/CoreSimulator/Devices/…
  Reason: image not found

If you try and run the app on a iOS device at all, whether with Xcode attached or not, you might also end up with an image not found error:

dyld: Library not loaded: @rpath/….framework/…
  Referenced from: /var/mobile/Containers/Bundle/Application/…/….app/…
  Reason: image not found

And most infuriating of all, once you come to create an archive of your app and attempt to validate it for TestFlight or the iOS App Store, you might end up with an error about missing BCSymbolMap files:

An error occurred during validation
The archive did not contain <DVTFilePath:…:‘…/Library/Developer/Xcode/Archives/….xcarchive/BCSymbolMaps/….bcsymbolmap’> as expected.

Searching the Internet has yielded increasingly complicated, and convoluted solutions up to, and including, custom build scripts. None of these were palatable, and after some experimentation, I believe I’ve found the way to make this work perfectly!

First you’ll want to create a new workspace to keep everything together. Use File → New → Workspace…, create a New Folder for your workspace to live in, and pick a name for the workspace. This will give you a new Xcode window, which unlike a usual project, contains no targets or files.

Now we’ll create the project for the first iOS app. With the workspace open, use File → New → Project…, pick the template, and enter the product name, etc. as usual. When you come to choose the location, make sure that the Add to: box has the workspace you selected.

This will return you to your workspace, but now with your new project added to it. Do it again for the second project, and be doubly careful at the last screen. The Add to: box will default to the workspace, but now there’s a new Group: box, and that defaults to the project you just created, not the workspace, so make sure you change it!

Now we can add the framework for the shared code. Once again use File → New → Project…, and this time pick the Cocoa Touch Framework project type.

Give your framework a name on the next screen, and again choose a location within your workspace folder, with the Add to: box containing the workspace, and changing the Group: box from the last project, to the workspace:

Now you have a workspace containing your two applications, and a framework for the code that you want them to share. The next bit is the tricky bit; we need both apps to depend on the framework, and while there are a number of ways of doing this, only one seems to work consistently for me without running into any of the warnings or errors above.

First you’ll want to reveal the Products of the framework you just added, and note that the final framework product is red, indicating that it doesn’t yet exist.

This is the first thing we want to fix, if we try and set things up without it being built, Xcode sometimes does the wrong thing! From the Scheme menu, select the framework itself, and from the Destination menu, select Generic iOS Device. Hit ⌘B or Product → Build to build the framework itself, and note that it turns black in the files list.

It’s safe to add it to the app. Select your first iOS app target, make sure the General tab is selected, and scroll until you can see Embedded BinariesDrag the framework from the left hand list, and drop it into this section. I’ve found that dragging, rather than using the “+” button; and using the Embedded Binaries section, rather than Linked Frameworks and Libraries one, is key to making Xcode work.

This will not just add it as an embedded binary, it will also add it to the Linked Frameworks and Libraries section as well, and will create a copy of the framework under the app project in the left hand list.

We’re not quite there… one more thing to do, select that copy of the framework from the files list for the app project—not the framework project—and make sure you can see the File Inspector. Note that the Location currently has Absolute Path, you need to change this to Relative to Build Products:

Do the same sequence for your second app.

By following these steps:

  • the App depends on the Framework, so builds with the latest version of the Framework source each time;
  • the iOS Simulator correctly embeds the Debug-iphonesimulator version of the Framework,
  • which means that the app will run both when attached to Xcode, and when not attached;
  • when built for the iOS Device, it correctly embeds the Debug-iphoneos version of the Framework,
  • so the app runs on the device both when attached to Xcode, and when not attached;
  • and the correct target versions are built and embedded when archiving,
  • which means you can validate and upload with both app symbols and bitcode.