Leave a rating/review
One of the most powerful SwiftUI components is the Stack.
You’ve already seen how easy it can be to add a VStack component and place just about anything you wanted in there, but how exactly do they work?
Depending on which way you want your UI elements to flow, there are 3 types of stacks to chose from: the two most common being HStack for horizonally-aligned Views and VStack for vertically-aligned Views. The last one is a ZStack for views stacked by depth.
If you look at the documentation for VStack, it isn’t immediately obvious how exactly you can compose these types of multi-statement components nested inside of the Stack view. But there’s that ViewBuilder property wrapper again!
This @ViewBuilder is what’s allowing a Stack to process multiple components inside the closure, without any additional syntax and generate the View you want.
For now, all you need to know is that you can and should use Stacks when creating more complex layouts, even nesting multiple Stacks within each other.
In this episode you’re going to create a bit of a riff on a section of the Apple Music app. I’m pretty fond of my weekly “Get Up! Mix”, so you’ll make a header for a Meow! Mix for cats. I’m basing it on the layout I see on my iPad, so choose an iPad simulator for this one, I’ll do the 12.9 inch iPad Pro.
So far you’ve used VStacks without any arguments, like this one!
- If you look at the initializer in the documentation, you’ll see you can also specify an alignment and spacing.
The “Content” is the closure you’ve just been casually adding views to.
Without an alignment parameter the stack will default to centering its content along its axis.
- But you’ve also got
leadingandtrailingas options, which you can choose from this nifty interface in the attributes inspector. - All of our text should be lined up on the leading side.
One of the incredible features of Stacks is Adaptive Spacing.
When you initialize your Stacks you have the option of specifying the exact Spacing you want.
But if you leave it blank, the layout system will automatically apply the correct spacing for baseline to baseline text spacing and baseline to edge spacing that precisely follows Apple’s Interface Guidelines for you!
You want that automatic spacing for the first three bits of text. But the last one should have a bit of space.
There’s several ways you could do this. You might consider padding! But you’re going to do it by adding another VStack with a custom Spacing.
- So, embed this VStack in a VStack
- And set the Alignment to Leading
- And the Spacing to 20
VStack(alignment: .leading, spacing: 20.0) {
Then move the last Text component outside of the inner VStack. Very cool!
Now let’s try nesting different types of stacks.
Create an HStack below all of this text.
HStack {
}
This horizontal stack will try to evenly distribute its content across the Horizontal axis, as opposed to the vertical.
You’re going to put a pair of buttons in here! But as you can see from the example, they’ll look pretty similar. So, let’s start with one and then you can try extracting the view!
Use the button initializer with an Action and label
Button { print("Play!") }
label: {
}
It won’t really do anything, but you can add a print statement in the action if you like.
Inside the button’s label, we’ll use a Label type. A SwiftUI Label has an initializer that takes a String for a title and a string to grab a system Image.
Label("Play", systemImage: "play.fill")
And it will lay those two things out next to each other for you! Nice.
Now, you want this label to sit in the middle of a big gray button. You’re actually going to use another HStack to get that working.
- So, embed the label in an HStack
- And then put a “Spacer” on either side of it.
Spacer()
Label(...)
Spacer()
The top view in an HStack will be the leading-most view in the layout. And a “Spacer” is a special type of empty view that evenly fills up as much space as you need it to.
You can’t really see what that means right now, so, let’s add some more modifiers to style this and make it more obvious what’s going on.
- Add some padding, just to the vertical dimensions
- Then a gray background with an opacity of 15%
- and add rounded corners with a radius of 12
HStack {
...
}
.padding(.vertical)
.background(Color.gray.opacity(0.15))
.cornerRadius(12)
Now you can see that this button stretches all the way across the screen horizontally. That’s the spacers at work!
You want another button nearly exactly like this, but you don’t want to have to repeat all of this code when the only thing changing for the label is the title and system image.
- So, extract this HStack!
- call it “ButtonLabel”
- then set up a pair of properties to pass the title and system image along
- Add pass that into the Label itself.
struct ButtonLabel: View {
let title: String
let systemImage: String
var body: some View {
...
Label(title, systemImage: systemImage)
Now, back up in ContentView, fill the play button’s arguments in
label: { ButtonLabel(title: "Play", systemImage: "play.fill") }
And then copy the whole button and make another one for Shuffling!
Button { print("Shuffle!") }
label: { ButtonLabel(title: "Shuffle", systemImage: "shuffle") }
- To put a little space between these two, adjust the HStack they’re in so it has a spacing of 12 points
ZStacks
The third kind of Stack is called a ZStack. It stacks views depth-wise, like this big orange square with the text and play symbol in it.
You’re actually going to start by making that its own separate view.
- So, start a brand new struct just above ButtonLabel and call it “Thumbnail”, and it should conform to “View”.
- a View requires a body, and, as you’ve talked about, that should be “some View”
struct Thumbnail: View {
var body: some View {
- To get our new Thumbnail previewing, add it to the
previews - You can set a custom preview layout for each item in here. You’ll set a fixed, square size for this one, with a little padding so you can see the drop shadow, later.
Thumbnail()
.padding()
.previewLayout(.fixed(width: 250, height: 250))
- Inside the body, make a ZStack! Use the initializer with Alignment and Content
- And set up your trailing closure syntax for that content
ZStack(alignment: Alignment(horizontal: .center, vertical: .center)) {
}
You can learn more about ZStacks in the Layout course later in this learning path, but, for now, you can define how you want its contents aligned in both the vertical and horizontal axes.
You want leading for horizontal, and top for vertical, so that the text will sit in the upper left corner
Alignment(horizontal: .leading, vertical: .top)
- But you also want this play symbol to sit in the center, so make one more plain ZStack without the alignment defined. That will use centering by default
- Inside of that stack, add a rounded rectangle shape with a corner radius of 12
- Use the “accent color” for the foreground
- You always want this to be a square, so here’s a new modifier for you: Aspect Ratio. 1 will give you the square you want, use “fit” for the content mode
- And give it a shadow!
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.orange)
.aspectRatio(1, contentMode: .fit)
.shadow(radius: 10)
The component at the top of the ZStack list will be the “bottom” or deepest component in the stack.
- So, to get a play symbol to appear, just add it below the rounded rectangle.
- Make it resizable, and set the frame to something small-ish, like 50
- Add a white foreground color
- And finally drop the opacity to 0.4
Image(systemName: "play.fill")
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.white)
.opacity(0.4)
On top of that, but in the outer ZStack, add a new VStack with leading alignment for the Meow! Mix title.
- Make the Meow! a large title, with a black weight
- And the Mix a regular Large Title
- All of this text should be white, and if you change the foreground color on the entire VStack, it will do that for us.
- Let’s also go with a little padding around the stack.
VStack(alignment: .leading) {
Text("Meow!")
.font(.largeTitle)
.fontWeight(.black)
Text("Mix")
.font(.largeTitle)
Spacer()
}
.foregroundColor(.white)
.padding()
Put it together!
Now you just need to put it all together.
You want the thumbnail to be on the leading side of all this text, horizontally,
- so embed that VStack in an HStack
- And then make an instance of our Thumbnail view!
- For this HStack, you want to align everything on the bottom, and give it some explicit spacing. I’ll use 20 points again.
HStack(alignment: .bottom, spacing: 20) {
Thumbnail()
This is really close, but it’s kind of hard to fix, because you can’t see the bigger structure of our layout with all of this code.
- First, extract the buttons out into a Buttons subview
- Move the buttons into that parent VStack
- Then extract out all the text into a “MixDescription” subview
Ah! That’s a little easier to read now.
I’m going to show you how to set a sort of range for sizing your views with a frame.
- Add a frame modifier to the HStack, and this time, in code, pick the one with all of these arguments.
- I want to say that this header we’re building should always be at least 150 points tall, but no taller than 250 points.
- You only need to specify the values you want. Any values not specified here will use their default behavior. So, just keep MinHeight and MaxHeight.
- And add a little padding before that frame is applied.
}
.padding()
.frame(minHeight: 150, maxHeight: 250)
And then add a VStack around the description and buttons, with a leading alignment, to get those looking right.
One more trick for you. If you want to see what this would all look like pushed up against the top of the iPad,
- embed the outer HStack in a VStack.
- And add a spacer to bottom!
Stacks are amazing! You can create just about anything with them by nesting them together and they handle spacing for you depending on the environment they’re displayed in.
But they aren’t the only layout tool at your disposal!
When looking at examples, it’s easy to see nesting stacks, but coming up next you’re going to try making your components more reusable to avoid quite so much embedding.