Lesson 6 • Intermediate
SwiftUI Basics 📱
By the end of this lesson you'll be able to build a real iOS screen by hand: describe it with a View, stack and style the pieces with modifiers, add a tappable button, and make the screen update itself with @State.
What You'll Learn in This Lesson
- • Write a
Viewstruct with abodythat describes the screen - • Show content with
TextandImage(SF Symbols) - • Arrange views using
VStack,HStack, andZStack - • Style anything by chaining modifiers like
.font,.padding,.foregroundColor - • Add a
Buttonthat runs an action when tapped - • Make the UI react automatically to data with
@State
🧠 Declarative vs Imperative
In the old imperative style you wrote every step: create a label, set its text, add it to the view, then later find that label and change it by hand each time the data moved. That's a lot of bookkeeping, and bugs creep in when the screen and the data drift out of sync.
SwiftUI is declarative: you write one body that says what the screen looks like for the current state. When the state changes, SwiftUI re-runs body and updates only what actually changed. You describe the what; the framework handles the how.
1️⃣ Your First View: Text, Image & Modifiers
Everything you see in SwiftUI is a View — a lightweight struct that describes something on screen. Every view struct has a body that returns the content. You customise a view by chaining modifiers: methods like .font(), .foregroundColor(), and .padding() that each return a new, tweaked view. Read this worked example, then build it.
import SwiftUI
// Every screen is a struct that conforms to the View protocol.
// The 'body' property describes WHAT to show — SwiftUI builds it.
struct WelcomeView: View {
var body: some View {
// A vertical stack places its children top-to-bottom.
VStack {
// An SF Symbol icon (Apple ships thousands for free).
Image(systemName: "swift")
.font(.system(size: 60)) // make the symbol big
.foregroundColor(.orange) // tint it orange
// A line of text, customised by chaining modifiers.
Text("Hello, SwiftUI!")
.font(.largeTitle) // big title text style
.bold() // make it bold
.foregroundColor(.blue) // colour the text blue
}
.padding() // breathing room around the stack
}
}On screen: a centred column showing a large orange Swift
logo, and directly below it the words "Hello, SwiftUI!" in
big, bold, blue title text — with empty space (padding)
around the whole group.2️⃣ Layout with Stacks
You arrange views with three stacks. VStack lays children out top-to-bottom, HStack left-to-right, and ZStack layers them back-to-front. You nest stacks inside each other to build any layout — no manual constraints required. The spacing: argument controls the gap between children.
import SwiftUI
// Three stacks arrange views; you nest them to build any layout:
// VStack — top to bottom HStack — left to right
// ZStack — back to front (layered)
struct ProfileCard: View {
var body: some View {
VStack(spacing: 12) { // vertical, 12pt between rows
Image(systemName: "person.circle.fill")
.font(.system(size: 70))
.foregroundColor(.blue)
Text("Alice Johnson")
.font(.title2)
.bold()
// A horizontal row of two labels, side by side.
HStack(spacing: 30) { // left to right
Text("142 Posts")
Text("1.2k Followers")
}
.font(.subheadline)
.foregroundColor(.gray)
// ZStack stacks views on top of each other (back to front).
ZStack {
Color.blue // back layer: a blue rectangle
Text("iOS Developer") // front layer: text on top
.foregroundColor(.white)
.padding(8)
}
.cornerRadius(8)
}
.padding()
}
}On screen: a centred card. From top to bottom — a blue
person icon, the bold name "Alice Johnson", then a single
row with "142 Posts" and "1.2k Followers" side by side in
small grey text, and finally a rounded blue banner with the
white words "iOS Developer" sitting on top of it.Your turn. The view below is almost finished — fill in the three blanks marked ___ using the hints, then preview it.
import SwiftUI
struct GreetingView: View {
var body: some View {
// 🎯 YOUR TURN — replace each ___ then preview it in Xcode.
Text("Good morning!")
// 1) Make the text use the large-title style
.font(___) // 👉 .largeTitle
// 2) Colour the text green
.foregroundColor(___) // 👉 .green
// 3) Add space around the text so it isn't cramped
.___() // 👉 padding
// ✅ Expected on screen:
// large green "Good morning!" text with padding around it.
}
}___ with the value in its // 👉 hint, then check the result against the "Expected on screen" comment in an Xcode preview.3️⃣ Buttons & @State: a Reactive Counter
A Button takes a title and an action — a closure (a block of code in { }) that runs on tap. To make the screen respond, you store data in a property marked @State. A SwiftUI view is a struct that gets rebuilt constantly, so @State tells SwiftUI to keep that value safe between rebuilds and to re-run body automatically whenever it changes. That automatic refresh is what "reactive" means.
import SwiftUI
// @State holds a value that BELONGS to this view. When it changes,
// SwiftUI automatically re-runs body and redraws the screen.
struct CounterView: View {
@State private var count = 0 // start at 0, owned by the view
var body: some View {
VStack(spacing: 20) {
// \(count) drops the current value into the text.
Text("Count: \(count)")
.font(.largeTitle)
// A Button takes a title and an action closure { ... }.
Button("Tap me") {
count += 1 // change state -> view refreshes
}
.buttonStyle(.borderedProminent) // a filled, tappable button
}
.padding()
}
}On screen: "Count: 0" in large text with a filled "Tap me"
button under it. Each tap increases the number — tap three
times and the label reads "Count: 3". You never told the
label to update; changing @State did it for you.Now wire up your own state. Fill in the two blanks so the heart toggles between empty and filled each time the button is tapped:
import SwiftUI
struct LikeButton: View {
// 1) Declare a @State Bool that starts as false
@State private var liked = ___ // 👉 false
var body: some View {
VStack(spacing: 16) {
Image(systemName: liked ? "heart.fill" : "heart")
.font(.largeTitle)
.foregroundColor(.red)
Button("Toggle like") {
// 2) Flip the value so the heart fills / empties
liked = ___ // 👉 !liked
}
}
// ✅ Expected on screen: an outline heart that becomes a
// filled red heart (and back) each time you tap the button.
}
}___ blanks using the // 👉 hints, then tap the button in an Xcode preview and confirm the heart fills and empties.Common Errors (and the fix)
- "Cannot assign to property: 'self' is immutable" — you tried to change a plain
varfrom insidebody. A view is a struct, so its stored values can't be mutated directly. Mark the property@State private varand SwiftUI will let you change it (and redraw the view). - Modifier order changes the result.
.padding().background(.blue)pads first then colours the larger area (a blue border);.background(.blue).padding()colours first then adds clear space. When a layout looks off, swap two modifiers. - "Type 'MyView' does not conform to protocol 'View'" — you forgot
: Viewon the struct, orbodydoesn't return a view. Declarestruct MyView: Viewand makebodyreturn exactly one view. - "Function declares an opaque return type 'some View'…" — your
bodyreturns two or more views at the top level. Wrap them in aVStack(or other stack) so it returns a single view.
Pro Tips
- 💡 Use the Canvas preview (⌥⌘P) to see changes instantly without building and running.
- 💡 Mark
@Stateasprivate— that state belongs to this view alone; other views shouldn't reach in. - 💡 Browse SF Symbols in Apple's free SF Symbols app to find icon names for
Image(systemName:).
📋 Quick Reference — SwiftUI
| Piece | Usage |
|---|---|
| Text | Text("Hi").font(.title) |
| Image | Image(systemName: "star.fill") |
| VStack | VStack { Text("A"); Text("B") } |
| HStack | HStack { ... } (side by side) |
| ZStack | ZStack { ... } (layered) |
| Button | Button("Tap") { count += 1 } |
| @State | @State private var count = 0 |
| padding | .padding() / .padding(16) |
Frequently Asked Questions
Q: What is the difference between declarative and imperative UI?
Imperative UI (like older UIKit) means you write step-by-step instructions to create a label, set its text, add it to the screen, and then update it by hand whenever data changes. Declarative UI (SwiftUI) means you describe what the screen should look like for any given state, and the framework figures out the steps and the updates for you. You write what, not how.
Q: Why does my view say it must conform to 'some View'?
Every SwiftUI view is a struct that adopts the View protocol, and that protocol requires a body property of type 'some View'. If you forget ': View' on the struct, or your body returns nothing, the compiler complains. The fix is to declare 'struct MyView: View' and make sure body returns exactly one view (wrap multiple views in a stack).
Q: Why can't I just change a normal variable to update the screen?
A SwiftUI view is a value type (a struct) that gets thrown away and rebuilt constantly, so a plain 'var' would lose its value on every redraw and changing it would not trigger anything. Marking a property with @State tells SwiftUI to store that value outside the struct and re-run body whenever it changes — that is what makes the UI reactive.
Q: Does the order of modifiers actually matter?
Yes, almost always. Modifiers wrap the view from the inside out, so .padding().background(.blue) adds space first and then colours the larger area (you get a blue border), whereas .background(.blue).padding() colours the view first and then adds clear space around it. When a layout looks wrong, try swapping two modifiers.
Q: Do I need a Mac and Xcode to follow along?
SwiftUI renders to a real iOS/macOS screen, so the proper tool is Xcode on a Mac, where the Canvas preview shows your view live as you type. If you only want to experiment with the Swift code itself you can use an online playground, but to actually see the interface you will want Xcode's preview.
Mini-Challenge: Quantity Stepper
No blanks this time — just a brief and an outline. Build a small view that shows a number with a minus button on its left and a plus button on its right, where tapping each updates the number live. You'll combine a VStack, an HStack, two Buttons, and one @State value.
import SwiftUI
struct StepperCard: View {
// 🎯 MINI-CHALLENGE: a quantity picker
// 1. Add a @State Int called "quantity" starting at 1.
// 2. In a VStack, show Text("Quantity: \(quantity)") in a title font.
// 3. Add an HStack with two buttons: "−" and "+".
// - "+" does quantity += 1
// - "−" does quantity -= 1 (don't worry about going below 0 yet)
//
// ✅ Expected on screen: a number with a − button on its left and a
// + button on its right; tapping each changes the number live.
var body: some View {
// your code here
Text("Replace me")
}
}🎉 Lesson Complete!
- ✅ A screen is a
Viewstruct whosebodydescribes the content - ✅
TextandImageshow content; modifiers like.font,.padding,.foregroundColorstyle it - ✅
VStack,HStack, andZStackarrange views — nest them for any layout - ✅ A
Buttonruns an action closure when tapped - ✅
@Statemakes the UI reactive: change the value and SwiftUI redraws automatically - ✅ SwiftUI is declarative — you describe the result, not the steps
- ✅ Next lesson: Working with APIs — fetch real data and show it in your app
Sign up for free to track which lessons you've completed and get learning reminders.