Section 3 · Building Screens with SwiftUI
Views and Modifiers
17 min3 min readChoose wisely
Lesson at a glance
Build SwiftUI views as small structs, lay them out with stacks, style them with modifiers in the right order, and check them instantly with previews.
- 1A view is a struct with a body
- 2Stacks and spacers
- 3Modifiers wrap views
- 4Logic in a body
- 5Previews
- 6Small views, composed
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
If you’ve used UIKit, or manipulated the DOM by hand, you’re used to telling the screen what to change: set this label’s text, hide that button, insert a row. SwiftUI flips that around. You describe what the screen should look like for the current data, and SwiftUI works out what changed and updates the pixels. Your job shrinks to writing that description well.
A view is a struct with a body
Here is the row Pocket Budget shows for each expense, using the Expense and Category types from the previous section:
import SwiftUI
struct ExpenseRow: View {
let expense: Expense
var body: some View {
HStack(spacing: 12) {
Image(systemName: expense.category.symbolName)
.font(.title3)
.frame(width: 32)
.foregroundStyle(.tint)
VStack(alignment: .leading, spacing: 2) {
Text(expense.title)
.font(.headline)
Text(expense.date, format: .dateTime.day().month())
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Text(expense.amount, format: .currency(code: "USD"))
.font(.body.monospacedDigit())
}
.padding(.vertical, 4)
}
}ExpenseRow conforms to the View protocol, whose one requirement is body. It’s a struct, so it’s cheap to create, and SwiftUI creates and discards them constantly. Don’t think of a view struct as the thing on screen; think of it as a recipe that SwiftUI reads whenever the data changes.
some View is the opaque type from the protocols lesson. The real type of that body is a deeply nested generic built from every stack and modifier, and some View lets the compiler track it without you writing it out.
Stacks and spacers
Three containers handle most layouts. HStack places children left to right (right to left in Arabic and Hebrew, automatically), VStack top to bottom, and ZStack layers them front to back. alignment and spacing control how children line up. Spacer() expands to fill free space, which is how the amount ends up pinned to the trailing edge.
Text can format values for you: Text(expense.amount, format: .currency(code: "USD")) and Text(expense.date, format: .dateTime.day().month()) follow the user’s locale, so a user in Cairo or Berlin sees separators and date order they expect. .monospacedDigit() keeps columns of amounts aligned.
Modifiers wrap views
.font(.headline) doesn’t change the text in place. It returns a new view that wraps the text and applies a font. Chain three modifiers and you’ve built three layers. That’s why order matters:
Text("Food")
.padding()
.background(.blue) // blue covers the text AND the padding
Text("Food")
.background(.blue) // blue covers only the text
.padding() // clear space outside the blueRead a modifier chain from top to bottom as “take this, then wrap it in that”. Some modifiers, such as .font and .foregroundStyle, flow down to every child, which is why setting .font(.title3) on a VStack styles all the text inside it.
Logic in a body
Because body is a description, plain Swift decides what’s in it. An if includes a view only when a condition holds, and a switch picks between views:
HStack {
Text(expense.title)
if expense.amount > 100 {
Image(systemName: "exclamationmark.circle")
.foregroundStyle(.orange)
.accessibilityLabel("Large expense")
}
}There’s no “show” or “hide” call anywhere. When the amount changes, SwiftUI reads body again and the icon appears or disappears on its own. Keep a body free of side effects, such as network calls or printing, because SwiftUI may read it many times and at moments you don’t control.
Previews
Add a preview to the bottom of the file:
#Preview {
ExpenseRow(expense: Expense(title: "Groceries", amount: 64.2, category: .food, date: .now))
.padding()
}Xcode renders it in the canvas next to your code and updates as you type. Previews are the fastest feedback loop in iOS development. Add one for each interesting state, such as a long title, a large amount or right-to-left layout, and you catch layout bugs before you ever launch the simulator.
Small views, composed
When a body passes about a screenful, extract pieces into their own structs:
struct CategoryBadge: View {
let category: Category
var body: some View {
Text(category.title)
.font(.caption.bold())
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(.blue.opacity(0.15), in: Capsule())
.foregroundStyle(.blue)
}
}Now CategoryBadge(category: expense.category) can appear in the row, the detail screen and a filter bar, styled identically. Extraction is free at runtime and gives SwiftUI smaller units to compare when data changes.
Everything in this lesson shows fixed data. Real screens change: the user types, toggles and adds expenses. The next lesson introduces state, the data that drives those changes, and how SwiftUI knows when to redraw.
Key takeaways
- A SwiftUI view is a lightweight struct whose
bodydescribes what the screen should look like for the current data. - VStack, HStack and ZStack arrange children vertically, horizontally and in layers;
Spacerpushes content apart. - Each modifier wraps the view in a new one, so order matters;
paddingbeforebackgroundpaints behind the padding. - Extract a subview as soon as a
bodygets hard to read; small views cost nothing at runtime.