Views and modifiers
Think of ordering a coffee. You don’t walk behind the counter and operate the machine; you say “a large latte with oat milk”, and the barista makes it. If you change your mind, you describe the new drink.
SwiftUI is declarativeDeclarative UIA UI style where you describe what the screen should look like for the current data, and the framework updates the screen when the data changes.Open in glossary → in the same way. You describe what the screen should look like for the current data, and SwiftUI builds it. When the data changes, you don’t edit labels by hand: SwiftUI asks your code for a new description and updates the screen to match.
A SwiftUI View is a lightweight value that describes UI; it isn’t the UI. SwiftUI keeps its own long-lived tree (the attribute graph), diffs new descriptions against it, and renders through UIKit/AppKit or its own drawing. View structs are created and thrown away constantly, which is why they must be cheap and free of side effects.
body is declared @ViewBuilder and returns some View: an opaque type whose concrete form is a deeply nested generic like ModifiedContent<ModifiedContent<Text, _PaddingLayout>, _BackgroundStyleModifier<Color>>. That static type is what lets SwiftUI diff efficiently, and it’s why modifier order is part of the type.
Your first view
import SwiftUI
struct GreetingView: View {
let name: String
var body: some View {
Text("Hello, \(name)!")
.font(.title)
.foregroundStyle(.blue)
}
}
#Preview {
GreetingView(name: "Ana")
}Line by line:
struct GreetingView: Viewdeclares a view. Views are structs, andViewis the protocol they follow.bodydescribes what the view shows.some Viewmeans “some kind of view; the compiler knows exactly which”.Text,.font(.title)and.foregroundStyle(.blue): a text view, then two modifiersView modifierA method like .padding() or .background() in SwiftUI that wraps a view in a new view with a changed look or behavior.Open in glossary → that change how it looks.#Previewshows the view in Xcode’s canvas while you type, without running the app.
Modifiers wrap views
Every modifier returns a new view that wraps the one before it. So the order you write them in is the order they’re applied, from the inside out:
// Blue fills the padding too: padding first, then the background behind it all
Text("Continue")
.padding()
.background(.blue)
// Blue only behind the text: the padding is added outside the blue
Text("Continue")
.background(.blue)
.padding()Read a chain of modifiers like wrapping a present: .padding() puts the text in a slightly bigger box, and .background(.blue) paints whatever is inside its box. Paint first and then add the box, and the extra space stays unpainted.
Common views and modifiers
| View | What it shows |
|---|---|
Text("Hi") | Text |
Image(systemName: "star.fill") | An SF Symbol icon (thousands come with iOS) |
Image("logo") | An image from your asset catalog |
Button("Save") { … } | A tappable button; the closure runs on tap |
Color.red, Rectangle(), Circle() | Colors and shapes; they fill the space they’re given |
| Modifier | Effect |
|---|---|
.font(.headline) | Text style; .title, .body, .caption… follow the user’s text size setting |
.foregroundStyle(.secondary) | Color of text and icons |
.padding(), .padding(.horizontal, 24) | Space around the view |
.background(.yellow) | Something drawn behind the view |
.frame(width: 120, height: 44) | A box of a given size around the view |
.clipShape(.rect(cornerRadius: 12)) | Cuts the view to a shape |
Some modifiers, like .font, also work on a container: VStack { … }.font(.caption) sets the font for every text inside it. Others, like .padding(), only affect the view they’re attached to.
Predict the output
How big is the yellow area?
Text("Hi")
.frame(width: 200, height: 100)
.background(.yellow).frame(width: 200, height: 100) wraps the text in a 200 × 100 box (the text sits in its center), and .background(.yellow) comes after it, so it paints the whole box. Swap the two modifiers and only the text gets a yellow background, centered in an invisible 200 × 100 box.
What a modifier chain really is
Each modifier produces a ModifiedContent<Content, Modifier>. Some modifiers are layout wrappers (padding, frame, offset), some are environment writers (font, tint, foregroundStyle as a style), and some attach behavior (onTapGesture, task). Environment modifiers flow down to every descendant, which is why .font on a VStack works; layout modifiers only affect their own child.
Two consequences that show up in reviews:
.framedoesn’t resize the view; it proposes a size to its child and places the child inside a new box.Textstill takes only the space it needs, centered by default.ifin a modifier chain changes identity.if highlighted { view.border(.red) } else { view }gives two branches with different types; SwiftUI treats them as two views, tearing down state and animating with insert/remove instead of a smooth change. Prefer an always-present modifier with a changing value:.border(highlighted ? .red : .clear).
Deep diveWhy body can contain if and forInterview favorite
body is implicitly marked with @ViewBuilder, a result builder. The compiler rewrites the statements inside it into calls like buildBlock(a, b), buildEither(first:) for if/else and buildOptional for if without else. Each produces a concrete generic type (TupleView<(Text, Image)>, _ConditionalContent<A, B>, Optional<A>).
That’s why body can list several views without a container, and why a plain for loop doesn’t work: a result builder needs a statically known shape, so collections go through ForEach, which is itself a view.
- Views are cheap, short-lived value descriptions;
bodyis a@ViewBuilderreturning an opaquesome View. No side effects ininitorbody. - Each modifier wraps the view in
ModifiedContent: order is part of the type and of the result (.padding().background()≠.background().padding()). - Environment modifiers (
.font,.tint) flow down to descendants; layout modifiers (.padding,.frame) affect only their child. ifaround a modifier creates two identities; use a conditional value instead.
Check this part
Which code shows a blue background that includes the padding?
What does a view's
bodydo?Why should
if highlighted { v.border(.red) } else { v }be avoided?VStack { … }.font(.caption)changes the font of every Text inside. Why?