From this lesson on we build screens. SwiftUI has one big idea: a screen is a *function of its data*. You describe what the screen should look like for the current data, and SwiftUI redraws it whenever the data changes. You do not move buttons around by hand. This is called declarative UI, and it is the reason SwiftUI code is short.
Everything is a view
A view is a small piece of the screen: some text, an image, a button, a list, or a group of other views. A view is a struct that follows the View protocol and has a body property. The word some View means *a view of some kind, and the compiler works out which*. Views are cheap, so SwiftUI expects you to make many small ones and combine them.
import SwiftUI
struct ExpenseRow: View {
let title: String
let amount: Double
var body: some View {
HStack(spacing: 12) {
Image(systemName: "cart.fill")
.foregroundStyle(.white)
.frame(width: 36, height: 36)
.background(.blue, in: Circle())
VStack(alignment: .leading) {
Text(title).font(.headline)
Text("Today").font(.caption).foregroundStyle(.secondary)
}
Spacer()
Text(amount, format: .currency(code: "USD"))
.font(.headline)
}
.padding()
}
}
#Preview {
ExpenseRow(title: "Groceries", amount: 42.8)
}
Read it from the outside in. An HStack puts things side by side. It holds an icon, a VStack with two lines of text, a Spacer and the amount. The Spacer is a flexible gap that pushes the amount to the right edge. Image(systemName:) uses SF Symbols, thousands of free icons that come with Apple's systems. The Mac app named SF Symbols lets you browse them. Text(amount, format: .currency(code: "USD")) shows a number as money, and the currency code is a parameter, so you can change it.
The three stacks
VStacklays views out vertically, top to bottom.HStacklays views out horizontally, left to right. In languages that read right to left, SwiftUI flips this for you.ZStacklayers views on top of each other, with the first at the back. Use it for a badge on an image, or text over a colour.- Each stack takes
alignmentandspacing.Spacer()takes up all the space it can.Divider()draws a thin line.
Modifiers
A modifier is a method that returns a changed copy of a view: .font, .padding, .background, .frame, .foregroundStyle, .cornerRadius and many more. You chain them, one after another. The surprise for beginners is that the order matters, because each modifier wraps the result of the one before it.
// Background first, then padding: the yellow stays tight around the text.
Text("Hello")
.background(.yellow)
.padding()
// Padding first, then background: the yellow includes the padding.
Text("Hello")
.padding()
.background(.yellow)
Try both in the preview and look at the difference. When a layout looks wrong, the order of modifiers is the first thing to check. Some modifiers, such as .font, pass down to every view inside a stack, so you can set a font once on a VStack and not on each line.
Frames, sizes and safe areas
Views take as little space as they need. Use .frame(width:height:) for a fixed size, or .frame(maxWidth: .infinity) to stretch to the full width. A screen on an iPhone has safe areas, the parts not hidden by the notch, the rounded corners or the home bar. SwiftUI keeps your content inside them by default, which is what you want for text. A background colour often should go behind them, and .ignoresSafeArea() lets it.
struct SummaryCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text("This month")
.font(.subheadline)
.foregroundStyle(.secondary)
Text(1240.5, format: .currency(code: "USD"))
.font(.largeTitle.bold())
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16))
.padding(.horizontal)
}
}
Small views are a good habit
- When a
bodyis longer than about 30 lines, cut a piece out into its own struct, likeExpenseRowabove. Smaller views are easier to read and to preview. - Give a view only the data it needs, in
letproperties. That makes it easy to reuse and to test with a preview. - Use semantic styles such as
.font(.headline)and.foregroundStyle(.secondary), and not fixed sizes and colours. They adapt to Dark Mode and to people who set larger text in their phone's settings, which is part of making an app accessible.
Common mistakes
- A view with more than ten direct children in one stack. Group them with
Groupor split them into smaller views. - Forgetting that
Spaceronly expands inside a stack. - Fixed frame sizes for text. A bigger font makes the text cut off.
Your task
- Create a new SwiftUI file called
ExpenseRow.swiftwith the row above and change the icon and colours. - Create
SummaryCardand show it above the row inside aVStackinContentView. - Swap the order of
.padding()and.background()and write down what changes. - Turn on Dark Mode and a larger text size in the preview, and fix anything that looks wrong.
How to check it worked
- The row shows an icon on the left, two lines of text, and the amount pushed to the right edge.
- You can explain why modifier order matters with an example.
- Your screen still looks fine in Dark Mode.