SwiftUI for Beginners · Lesson 2 of 9

Building Your First Screen

In Lesson 1 you put a few views on screen. Now you'll learn how to arrange them: stacks, spacing, alignment, padding, backgrounds and frames. By the end you'll build a complete welcome screen — and, more importantly, learn how to look at any design and work out how to build it.

Thinking in Stacks

Open almost any app and look closely at one screen. You'll notice that nearly everything is lined up in one of two directions: top to bottom, or side by side. A title sits above a subtitle. An icon sits next to a label. A row of buttons runs across the bottom.

SwiftUI gives you a container for each direction — VStack for vertical and HStack for horizontal — plus ZStack for placing things on top of each other. With just these three, and a handful of modifiers, you can build a surprising number of real screens.

So the most useful question you can ask when looking at a design is:

Key idea

"Is this vertical or horizontal?" Answer that for the whole screen, then for each part inside it, and you'll know which stacks to use.

We'll come back to this question at the end of the lesson. First, let's get comfortable with each stack.

VStack: Top to Bottom

You met VStack in Lesson 1. It places its children one below the other, in the order you write them:

VStack {
    Text("Good Morning!")
    Text("Ready for a great day?")
}
   Good Morning!
Ready for a great day?
A VStack places views vertically and, by default, centers them.

As in Lesson 1, spacing controls the gap between children. VStack(spacing: 8) puts 8 points between each view; VStack(spacing: 0) puts them right against each other.

HStack: Side by Side

HStack is the horizontal version. Its children sit next to each other, from leading to trailing — left to right in English:

HStack {
    Image(systemName: "star.fill")
    Text("Today's Goal")
}
★ Today's Goal
An HStack places views horizontally.

This icon-plus-label pattern is everywhere: settings rows, menu items, buttons with icons. HStack takes a spacing value too, which controls the gap between the icon and the text.

Nesting Stacks

Here's where stacks become powerful: a stack can contain other stacks. Most real layouts are small stacks inside bigger ones.

Take a typical profile row: a picture on the left, and a name with a short description stacked on the right. The row as a whole runs horizontally. The name and description, on their own, run vertically.

HStack(spacing: 12) {
    Image(systemName: "person.circle.fill")
        .font(.largeTitle)

    VStack {
        Text("Maya Patel")
            .font(.headline)

        Text("iOS Developer")
            .font(.subheadline)
    }
}
HStack
│
├── Image          (profile picture)
│
└── VStack
    │
    ├── Text       ("Maya Patel")
    │
    └── Text       ("iOS Developer")
An HStack holds the image and a VStack; the VStack holds the two lines of text.

If you run this, you'll notice the two lines of text are centered relative to each other, which looks slightly off for a profile row. Names and descriptions usually line up on their left edge. That's what alignment is for.

Alignment

A stack's alignment decides how its children line up with each other. For a VStack, the options are about the horizontal edge: .leading, .center (the default) or .trailing.

VStack(alignment: .leading) {
    Text("Maya Patel")
        .font(.headline)

    Text("iOS Developer")
        .font(.subheadline)
}
.center (default)        .leading

   Maya Patel             Maya Patel
  iOS Developer           iOS Developer
Leading alignment lines up the left edges of the children.

You can pass alignment and spacing together, as in VStack(alignment: .leading, spacing: 4).

HStack has alignment too, but because its children sit side by side, the options are about the vertical edge: .top, .center or .bottom. That's useful when the items in a row have different heights.

Note

Alignment lines children up relative to each other. It does not push the whole stack to the edge of the screen. For that, you'll use Spacer or frame, both coming up next.

Spacer

A Spacer is an invisible view that takes up all the free space it can. Put one between two views, and it pushes them apart.

A classic example is a screen header with a title on the left and a settings icon on the right:

HStack {
    Text("Profile")
        .font(.title2)
        .fontWeight(.bold)

    Spacer()

    Image(systemName: "gearshape")
}
Profile |<------ Spacer ------>| ⚙️
The Spacer expands to fill the gap, pushing the title and the icon to opposite ends.

In an HStack, a Spacer pushes things apart horizontally. In a VStack, it pushes things apart vertically — for example, to move a button all the way to the bottom of the screen. You'll use exactly that in the final build.

Padding

Views sit right up against each other and against the screen edges unless you give them room. The .padding() modifier adds space around a view, inside whatever surrounds it.

Text("Learn something new today")
    .padding()

With no value, .padding() adds a comfortable system-standard amount on every side. You can also be more specific:

Text("Hello")
    .padding(24)             // 24 points on every side

Text("Hello")
    .padding(.horizontal)    // left and right only

Text("Hello")
    .padding(.top, 40)       // 40 points above only

Padding on its own is invisible — you only see its effect as extra space. It becomes much easier to see once the view has a background.

Backgrounds

.background() fills the area behind a view with a color:

Text("Learn something new today")
    .padding()
    .background(.blue.opacity(0.1))

.blue.opacity(0.1) is blue at 10% strength — a soft tint that keeps the text readable. To round the corners, clip the view to a rounded rectangle:

Text("Learn something new today")
    .padding()
    .background(.blue.opacity(0.1))
    .clipShape(
        RoundedRectangle(cornerRadius: 16)
    )

Now it looks like a card. There's one more modifier you'll use in the final build: .foregroundStyle(), which you met in Lesson 1. On a solid background you'll often set the text to white with .foregroundStyle(.white), so it stays readable.

Modifier Order Matters

Look at the padding and background example again. What happens if you swap the two lines?

// A: padding first, then background
Text("Hello")
    .padding()
    .background(.yellow)

// B: background first, then padding
Text("Hello")
    .background(.yellow)
    .padding()
A: .padding() → .background()

 ┌─────────────────┐
 │░░░░░░░░░░░░░░░░░│
 │░░░░░ Hello ░░░░░│
 │░░░░░░░░░░░░░░░░░│
 └─────────────────┘
 yellow fills the padded area


B: .background() → .padding()

      ┌───────┐
      │ Hello │
      └───────┘
 yellow hugs the text; the padding
 is empty space around it
Same modifiers, different order, different result.

The reason is that each modifier wraps everything above it. In A, the text is first given padding — it becomes a bigger box — and then that bigger box gets a yellow background. In B, the text gets a yellow background first, so only the text itself is yellow, and then empty padding is added around the outside.

A helpful way to picture it is wrapping a present: each modifier adds a new layer on the outside of whatever you already have. Read the modifiers from top to bottom, and imagine each one wrapping the result of the line before.

Key idea

Modifier order matters. Each modifier applies to the result of the modifiers above it.

Common mistake

Assuming .padding().background(…) and .background(…).padding() look the same. If a background looks too tight around your text, check the order: padding usually needs to come before the background.

Frame

Most views are only as big as their content needs. A Text is as wide as its words; an Image is as big as the symbol. The .frame() modifier lets you change that.

You can give a view a fixed size:

Image(systemName: "star.fill")
    .frame(width: 100, height: 100)

But the version you'll use most often lets a view stretch as wide as it's allowed to:

Text("Get Started")
    .frame(maxWidth: .infinity)
    .padding()
    .background(.blue)

maxWidth: .infinity means "take all the width available". The text stays centered, but its frame — and so its background — now stretches across the screen. That's how you make a full-width button shape.

You can also choose where the content sits inside a wider frame. .frame(maxWidth: .infinity, alignment: .leading) stretches the view but keeps its content against the leading edge — ideal for cards whose text should start on the left.

Tip

Modifier order applies here too. Put .frame(maxWidth: .infinity) before .background() if you want the background to stretch with it.

ZStack: Layering Views

The third stack doesn't go down or across — it goes toward you. A ZStack places its children on top of each other, like layers. The first view you write is at the back; each view after it sits in front.

ZStack {
    Circle()
        .fill(.blue.opacity(0.15))
        .frame(width: 100, height: 100)

    Image(systemName: "person.fill")
        .font(.system(size: 44))
        .foregroundStyle(.blue)
}
ZStack
│
├── Circle   ← back layer
│
└── Image    ← front layer

Result: a person icon sitting
on a soft blue circle
In a ZStack, later views are drawn in front of earlier ones.

.font(.system(size: 44)) sets an exact size in points, which is handy for a large decorative symbol. ZStack is great for avatars, badges and text over an image. You won't need it in every screen, but it's good to know it's there.

Building the Welcome Screen

Time to put everything together. Here's the screen we're building:

              ☀️

         Good Morning!
     Ready for a great day?

 ┌─────────────────────────────┐
 │ [star] Today's Goal         │
 │ Learn something new today   │
 └─────────────────────────────┘




 ┌─────────────────────────────┐
 │         Get Started         │
 └─────────────────────────────┘
The WelcomeView: a header, a goal card, and a button pinned to the bottom.

Before writing code, ask the question from the start of the lesson. Is the whole screen vertical or horizontal? Vertical — everything runs top to bottom. So the outer container is a VStack. Then break it into sections:

  1. An icon — a large sun symbol.
  2. A greeting — title and subtitle, one above the other: a small VStack.
  3. A goal card — a heading row (star beside text: an HStack) above a line of text, so a VStack aligned to the leading edge, with padding, a tinted background and rounded corners.
  4. Empty space — a Spacer pushes the button to the bottom.
  5. A button — full-width text with a blue background.

Now each section translates into code:

import SwiftUI

struct WelcomeView: View {
    var body: some View {
        VStack(spacing: 24) {
            Image(systemName: "sun.max.fill")
                .font(.system(size: 60))

            VStack(spacing: 8) {
                Text("Good Morning!")
                    .font(.largeTitle)
                    .fontWeight(.bold)

                Text("Ready for a great day?")
                    .font(.subheadline)
            }

            VStack(alignment: .leading, spacing: 12) {
                HStack {
                    Image(systemName: "star.fill")

                    Text("Today's Goal")
                        .font(.headline)
                }

                Text("Learn something new today")
                    .font(.subheadline)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
            .background(.blue.opacity(0.1))
            .clipShape(
                RoundedRectangle(cornerRadius: 16)
            )

            Spacer()

            Text("Get Started")
                .fontWeight(.semibold)
                .frame(maxWidth: .infinity)
                .padding()
                .background(.blue)
                .foregroundStyle(.white)
                .clipShape(
                    RoundedRectangle(cornerRadius: 14)
                )
        }
        .padding()
    }
}

Let's walk through the parts that combine several ideas:

The goal card

The card is a VStack(alignment: .leading, spacing: 12), so the heading row and the text line up on the left. Then four modifiers turn it into a card, and their order is deliberate:

  1. .frame(maxWidth: .infinity, alignment: .leading) stretches the card to full width while keeping its content on the left.
  2. .padding() adds breathing room inside the card.
  3. .background(.blue.opacity(0.1)) tints the whole padded, full-width area.
  4. .clipShape(RoundedRectangle(cornerRadius: 16)) rounds the corners.

Try moving .padding() below .background() and watch the text end up pressed against the edge of the tint — modifier order in action.

The "Get Started" button

The same recipe makes the button shape: stretch with .frame(maxWidth: .infinity), add .padding(), fill with .background(.blue), make the text white with .foregroundStyle(.white), and round the corners.

It only looks like a button for now — tapping it does nothing yet. Making things respond to taps is exactly what Lesson 3 is about.

The Spacer and the outer padding

The Spacer() between the card and the button takes up all the leftover height, which pushes the button to the bottom of the screen. Finally, .padding() on the outer VStack keeps everything away from the screen edges.

Tip

To preview this view in Xcode, change the preview at the bottom of the file to show it: #Preview { WelcomeView() }.

Breaking Down Any Screen

The code for the welcome screen is useful, but the process you used to write it is far more valuable. You didn't start by remembering APIs. You started by looking at the design and breaking it into smaller and smaller pieces:

SCREEN
  ↓
Sections
  ↓
Stacks
  ↓
Views
  ↓
Modifiers
How to approach any SwiftUI layout, from the outside in.
Key idea

When you look at any UI, work from the outside in:

  1. Screen — is the whole thing vertical or horizontal?
  2. Sections — what are the main blocks?
  3. Stacks — is each block vertical or horizontal?
  4. Views — which texts, images and shapes are inside?
  5. Modifiers — how is each one styled and spaced?

This works for screens far more complex than today's. The layouts get bigger, but the question stays the same: "Is this vertical or horizontal?" — and then break it into smaller sections.

Recap

What you learned
  • VStack arranges views vertically, HStack horizontally, and ZStack in layers.
  • Stacks can be nested; most layouts are small stacks inside bigger ones.
  • Alignment lines children up relative to each other; spacing sets the gap between them.
  • Spacer fills free space and pushes views apart.
  • .padding() adds space around a view; .background() fills behind it; .clipShape() rounds it.
  • .frame(maxWidth: .infinity) lets a view stretch to the available width.
  • Modifier order matters — each modifier wraps the result of the ones above it.
  • Build any screen by breaking it down: Screen → Sections → Stacks → Views → Modifiers.

Challenge

Your turn

Build a ProfileView that looks roughly like this:

         ( person )

        Your Name
      iOS Developer

 ┌─────────────────────────────┐
 │ [envelope]  you@example.com │
 │ [pin]       Your City       │
 └─────────────────────────────┘



 ┌─────────────────────────────┐
 │        Edit Profile         │
 └─────────────────────────────┘

Start by breaking it down before writing any code:

  • Is the whole screen vertical or horizontal?
  • Which part is a ZStack? (Hint: the avatar.)
  • Each row in the card is an icon beside text — which stack is that?
  • What pushes the button to the bottom?

Useful symbol names: "person.fill", "envelope" and "mappin.and.ellipse". Use your own name and details, and change the colors to whatever you like. If your card's background looks too tight, check your modifier order.