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:
"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?
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
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")
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
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.
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 ------>| ⚙️
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
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.
Modifier order matters. Each modifier applies to the result of the modifiers above it.
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.
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
.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 │
└─────────────────────────────┘
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:
- An icon — a large sun symbol.
- A greeting — title and subtitle, one above the other: a small
VStack. - A goal card — a heading row (star beside text: an
HStack) above a line of text, so aVStackaligned to the leading edge, with padding, a tinted background and rounded corners. - Empty space — a
Spacerpushes the button to the bottom. - 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:
.frame(maxWidth: .infinity, alignment: .leading)stretches the card to full width while keeping its content on the left..padding()adds breathing room inside the card..background(.blue.opacity(0.1))tints the whole padded, full-width area..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.
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
When you look at any UI, work from the outside in:
- Screen — is the whole thing vertical or horizontal?
- Sections — what are the main blocks?
- Stacks — is each block vertical or horizontal?
- Views — which texts, images and shapes are inside?
- 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
VStackarranges views vertically,HStackhorizontally, andZStackin layers.- Stacks can be nested; most layouts are small stacks inside bigger ones.
- Alignment lines children up relative to each other;
spacingsets the gap between them. Spacerfills 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
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.