SwiftUI: Layout & Interfaces

Nov 18 2021 Swift 5.5, iOS 15, Xcode 13

Part 2: Aligning Views

11. Challenge: Align Nested Stacks

Episode complete

Play next episode

Next
Save for later
About this episode
See versions

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 10. Stack Alignment Next episode: 12. Alignment Guides

This course was originally recorded in 2019. It has been reviewed and all content and materials updated as of October 2021.

In this episode, you’ll be recreating a layout from my favorite Twitter client: Tweetbot.

Specifically, you’ll be working with this section of a profile. We’ll be skipping the background view, because it’s a Scroll View — which is a topic for a later course — but we’ll tackle everything else here.

Your challenge is going to be to nest the required Stacks together, all with proper alignment. Before I issue that challenge, we’ll go over how to style up the basic Text and Image Views that will be the building blocks of the final design.

I’m going to be working with the reference image from the downloadable materials on the right. But it’s pretty tight, working this way. I’m only doing it because this is a screen recording. If possible, I recommend viewing the reference on another monitor, or an iOS device.

To start out with, I’ve provided you with one Image View and five text views, because typing out all of those Strings would have been boring. I also put them into a VStack, so they’d all be visible.

But that’s not necessarily to say that the end result will be a VStack. You’ll be figuring that out for yourself.

Lastly, for the preview, I added a background color, using a ZStack — which you’ll learn about later in the course.

So, now it’s your turn to jump in. Starting from the top, you’ll need to make the image resizable. One way to do that is through the Inspector. Control-option click on the view, click into the Add Modifier dropdown menu, then type “R-E-S”, and hit return.

      Image("RW")
        .resizable()

      Text("Following")

Back in the inspector, let’s give the image a 90 by 90 frame.

.frame(width: 90.0, height: 90.0)

Personally, I find those extraneous “dot-zeroes” slightly distracting, so I’ll use multiple cursors with shift-control-click, and delete all that.

.frame(width: 90, height: 90)

The next thing to do is round the image’s corners, and for that, we’ll need a radius. But we’ll need to reuse that for the white border around the image, so let’s store it in a constant.

  var body: some View {
    let cornerRadius

    VStack {

Its type needs to be CGFloat.

let cornerRadius: CGFloat

And, I’ve seen this in action already. I think 7 and a half points will look good.

let cornerRadius: CGFloat = 7.5

And because you’ve got something else in the function body now, other than the return value, you’ll need to add the return keyword.

return VStack {

Then you can apply the corner radius with a modifier of the same name — which I’ll just type out.

.frame(width: 90, height: 90)
.cornerRadius(cornerRadius)

If you have a big enough screen to work with, you’ll be able to see that better. Next, to add a white border, you’ll start with the overlay modifier, which puts one view in front of another.

.cornerRadius(cornerRadius)
.overlay(/*@START_MENU_TOKEN@*/ /*@PLACEHOLDER=Overlay Content@*/Text("Placeholder")/*@END_MENU_TOKEN@*/)

The view needs to be a rounded rectangle…

        .overlay(
          RoundedRectangle(cornerRadius: <#T##CGFloat#>)
        )

…with the same corner radius as the Image.

        .overlay(
          RoundedRectangle(cornerRadius: cornerRadius)
        )

But you don’t need a solid rectangle — you need a rectangular stroke.

          RoundedRectangle(cornerRadius: cornerRadius)
            .stroke(

… the content of which, is a solid white color.

.stroke(Color.white, lineWidth: <#T##CGFloat#>)

The line width I went with is 2.5 points.

.stroke(Color.white, lineWidth: 2.5)

That’s all for the Image. Now, to the Text. Control-option-click on “Following” and let’s use the Body font style, Bold weight, and White, for color.

        )

      Text("Following")
        .font(.body)
        .fontWeight(.bold)
        .foregroundColor(Color.white)

      Text("raywenderlich.com")

I don’t know why that puts in the Color type. Obviously it’s a color.

.foregroundColor(.white)

But there’s no “background color” modifier, so you do need to specify the type, to achieve a blue background.

.background( Color.blue )

You’ll need a little padding on the top and bottom…

        .background( Color.blue )
        .padding(., <#T##length: CGFloat?##CGFloat?#>)

…and you can choose vertical, which is a shortcut for making an Edge Set out of top and bottom.

.padding(.vertical, <#T##length: CGFloat?##CGFloat?#>)

4 looks good to me.

.padding(.vertical, 4)

…But you don’t need padding around the blue background. You need padding inside of it. So you need to move that modifier before the background.

        .foregroundColor(.white)
        .padding(.vertical, 4)
        .background( Color.blue )

And in the same place, add some horizontal padding as well — just a little thicker.

        .foregroundColor(.white)
        .padding(.horizontal, 10)
        .padding(.vertical, 4)

That’s done, except for being too blocky. To fix that, there’s the clipShape modifier.

.clipShape(<#T##shape: Shape##Shape#>)

Just provide it an instance of the Capsule type.

        .background( Color.blue )
        .clipShape( Capsule() )

      Text("raywenderlich.com")

The next Text view down is a lot simpler. Just give it the headline font and make it white.

      Text("raywenderlich.com")
        .font(.headline)
        .foregroundColor(.white)

Same thing for the next one, but it’s smaller, so subheadline works.

      Text("@rwenderlich")
        .font(.subheadline)
        .foregroundColor(.white)

Then, for “FOLLOWS YOU”, try the Caption font.

.font(.caption)

And it needs a foreground color,

      Text("FOLLOWS YOU")
        .font(.caption)
        .foregroundColor(<#T##color: Color?##Color?#>)

      Text(

…but that color is about 40% grey, which isn’t a built-in Color property. Instead, you can either type Color, or “dot-init”, to make a color with a white value ranging from zero to 1.

.foregroundColor( .init(white: 0.4) )

You can use the same initializer to make a lighter gray background. But this time, like before, you have to specify the Color type, because the background could be any type of view — it’s not restricted to colors.

.background( Color(white: 0.9) )

Clip that to be a capsule as well…

.clipShape( Capsule() )

…and give it some horizontal padding.

        .foregroundColor( .init(white: 0.4) )
        .padding(.horizontal, 10)
        .background( Color(white: 0.9) )

The last Text view is also going to need some padding, but only on the top, to separate it from that capsule.

.padding(.top, 5)

And, finally, Let’s tighten up that line spacing.

        .padding(.top, 5)
        .lineSpacing(<#T##lineSpacing: CGFloat##CGFloat#>)
    }

You need some kind of negative value for that. Might as well go for the most extreme one…

.lineSpacing(-.infinity)

I think those are all the views you need. They’re not an exact match to the reference, but they’re pretty close. Feel free to get them even closer if you want to whittle away — and let us know what you come up with, in the comments, if you like your results. But your challenge is to get these views in the right place. Using nested stacks, and alignment.

Pause the video, put that layout together, and then join me after the break, to see if we did it the same way.

So, how many Stacks did you end up using? For me, it was four. I started off by making the “broadest stroke”: creating an HStack that two VStacks could live inside. I did that by command-clicking on the existing VStack and choosing Embed in HStack.

    return HStack {
      VStack {

Then, I capped off that VStack, with a curly brace, right after the “Following” Text…

          .clipShape( Capsule() )
      }

        Text("raywenderlich.com")

…and started a new VStack, for the rest of the Text Views.

      }

      VStack {
        Text("raywenderlich.com")

And while the first VStack didn’t need manual alignment, because the default center is what I wanted, this new one needed leading.

      VStack(alignment: .leading) {
        Text("raywenderlich.com")

For the next, and final stack, I wanted to embed two text views together, but I don’t think that’s possible yet in Xcode. So I selected the “@rwenderlich” text, and embedded just that in an HStack.

        HStack {
          Text("@rwenderlich")
            .font(.subheadline)
            .foregroundColor(.white)
        }

Then I selected all the lines for the “FOLLOWS YOU” view, and hit option-command-left-bracket to move them up into the HStack.

            .foregroundColor(.white)

          Text("FOLLOWS YOU")
            .font(.caption)
            .foregroundColor( .init(white: 0.4) )
            .padding(.horizontal, 10)
            .background( Color(white: 0.9) )
            .clipShape( Capsule() )
        }

        Text(

That stack was really close to looking right, by default, but I did make an alignment tweak. Let’s zoom in so you can see. I wanted to align along the baseline of “FOLLOWS YOU”, and the easiest way to get that into code was to use the Inspector and add any manual option…

HStack(alignment: .center) {

…and then use autocomplete to change it to the lastTextBaseline that I actually wanted.

HStack(alignment: .lastTextBaseline) {

Finally, I specified an alignment for the outer HStack. With the profile description being this exact length, it’s not important, but what if it were shorter?

        Text(
          "We are a friendly and supportive community of mobile developers. "
//            + "We love to learn and share our knowledge with the world! "
//            + "raywenderlich.com"
        )

…Now, because of the default center alignment, the VStack on the right is too low. So, I ensured that it would always be in the right place, by using top alignment. (Clicking in between the VStacks was the easiest way to do that.)

            + "We love to learn and share our knowledge with the world! "
            + "raywenderlich.com"
return HStack(alignment: .top) {

Four stacks. Three manual alignment adjustments. Usable Twitter profile layout. The main missing thing is “raywenderlich.com” not being a link, but unfortunately that’s not easy to do in SwiftUI. So we’ll leave it for now, and dive deeper into alignment.