SwiftUI for Beginners Β· Lesson 3 of 9
Making Your Screen Interactive
In Lesson 2, we learned how to build layouts using:
VStack
HStack
ZStack
Spacer
padding
frame
background
Now we'll make those screens respond to the user.
By the end of this lesson, you'll understand how to build something like:
ββββββββββββββββββββββββββββββββ β β β Daily Progress β β β β 3 β β tasks completed β β β β [ Complete +1 ] β β β β Your Name β β ββββββββββββββββββββββββββ β β β Alex β β β ββββββββββββββββββββββββββ β β β β Daily Reminder ON β β β β Reset β β β ββββββββββββββββββββββββββββββββ
This introduces four important SwiftUI concepts:
Button β State β TextField β Toggle
But the real lesson is bigger:
When data changes, SwiftUI updates the screen.
1. Let's Start With a Button
A button can be incredibly simple:
Button("Tap Me") {
print("Button tapped!")
}
There are two parts here:
Button("Tap Me") {
β β
what we see what happens
}
When the user taps the button, Swift executes the code inside { }.
For example:
Button("Say Hello") {
print("Hello!")
}
Tap it five times and "Hello!" gets printed five times.
But printing something to Xcode isn't particularly exciting.
Let's make the screen itself change.
2. Meet @State
Suppose we want to create a counter:
0 [ Increase ]
When the user taps Increase:
0 β 1 β 2 β 3 β 4...
We need somewhere to store that number.
Normally in Swift, you might write:
var count = 0
In a SwiftUI View, however, local UI state is commonly declared using:
@State private var count = 0
Our view becomes:
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("\(count)")
.font(.largeTitle)
.fontWeight(.bold)
Button("Increase") {
count += 1
}
}
}
}
Run it.
Initially:
0
Tap the button:
1
Again:
2
Again:
3
But something very important just happened.
We never told the Text to update.
We only changed:
count += 1
SwiftUI took care of the rest.
3. This Is the SwiftUI Mental Model
This concept is worth slowing down for.
Our state is:
@State private var count = 0
Our interface uses that state:
Text("\(count)")
The button changes the state:
count += 1
So the flow is:
STATE
β
βΌ
count = 0
β
βΌ
UI
β
βΌ
0
User taps button
β
βΌ
count = 1
β
βΌ
SwiftUI updates
β
βΌ
1
Think of it as:
STATE β UI
Change the state:
STATE CHANGES
β
SwiftUI notices
β
UI reflects new state
This is one of the biggest differences from traditional UIKit programming.
Instead of thinking:
Find this label and manually change its text.
you increasingly think:
Change my data. The UI describes what that data should look like.
4. What Exactly Is @State?
Let's break this down:
@State private var count = 0
var count
This is our variable.
count
stores an Int.
private
private var count
means this state belongs to this view and isn't intended to be accessed directly from outside it.
That's generally a good habit for local view state.
@State
This is the special SwiftUI part.
@State
tells SwiftUI:
This value can change, and changes to it may affect this view's UI.
So:
@State private var count = 0
can be thought of as:
"
countis mutable state owned by this view."
5. Let's Add Another Button
We currently have:
3 [ Increase ]
Let's add Reset.
VStack(spacing: 20) {
Text("\(count)")
.font(.largeTitle)
.fontWeight(.bold)
Button("Increase") {
count += 1
}
Button("Reset") {
count = 0
}
}
Now:
0 [ Increase ] [ Reset ]
Tap Increase three times:
3
Tap Reset:
0
Both buttons are modifying the same state.
6. The UI Can Depend on State
State isn't only useful for displaying numbers.
It can control how the interface looks.
For example:
Text(count == 0 ? "Let's get started!" : "Great progress!")
Now when:
count = 0
the screen says:
Let's get started!
But after tapping Increase:
Great progress!
The UI is reacting to the data.
We can go even further:
Image(systemName: count == 0 ? "circle" : "checkmark.circle.fill")
Now the image can change too.
This is state-driven UI.
7. Showing and Hiding Views
State can even determine whether a view exists.
if count >= 5 {
Text("π Goal completed!")
.font(.headline)
}
Initially:
Count: 0
At 4:
Count: 4
At 5:
Count: 5 π Goal completed!
The interesting part is that we're not manually hiding or showing a label.
We're describing a rule:
If
countis at least 5, thisTextshould exist.
That's very SwiftUI-like thinking.
8. TextField β Let the User Enter Text
So far the user can tap things.
What about typing?
SwiftUI gives us TextField.
Let's create another piece of state:
@State private var name = ""
Then:
TextField("Enter your name", text: $name)
You'll notice something new:
$name
instead of:
name
Don't worryβwe'll come back to $ shortly.
Let's use it:
struct WelcomeView: View {
@State private var name = ""
var body: some View {
VStack(spacing: 20) {
TextField("Enter your name", text: $name)
.textFieldStyle(.roundedBorder)
Text("Hello, \(name)!")
}
.padding()
}
}
Type:
Sam
and immediately:
Hello, Sam!
appears.
Type:
Taylor
and the UI becomes:
Hello, Taylor!
Again:
User types
β
name changes
β
SwiftUI notices
β
Text updates
Same idea.
Different control.
9. What Does $name Mean?
This is one of those things that looks mysterious when you're new.
Compare:
Text("Hello \(name)")
with:
TextField("Name", text: $name)
Why does one use:
name
and the other:
$name
Because they need different things.
name
means:
Give me the current value.
If:
name = "Sam"
then:
Text(name)
needs to read "Sam".
$name
means roughly:
Give this control a connection to the value so it can read and change it.
A TextField needs that connection because when the user types, the text field needs to update name.
name
β²
β
βΌ
TextField
Changes can flow between them.
SwiftUI calls this connection a Binding.
For now, just remember:
name β current value $name β connection/binding to the value
We'll understand bindings much more deeply in a later lesson.
10. Toggle β Working With Bool
Suppose our app has a setting:
Daily Reminder ON
That's either:
true
or:
false
So we create:
@State private var reminderEnabled = false
Then:
Toggle("Daily Reminder", isOn: $reminderEnabled)
Now the user can switch it:
OFF β ON
and our state changes:
false β true
We can display that state too:
Text(reminderEnabled ? "Reminder is on" : "Reminder is off")
So once again:
Toggle changes
β
reminderEnabled changes
β
UI updates
Notice the pattern repeating?
That's intentional.
Let's Build an Interactive Screen
Now let's combine everything.
struct DailyProgressView: View {
@State private var count = 0
@State private var name = ""
@State private var reminderEnabled = false
var body: some View {
VStack(spacing: 24) {
Text("Daily Progress")
.font(.largeTitle)
.fontWeight(.bold)
VStack(spacing: 8) {
Text("\(count)")
.font(.system(size: 60))
.fontWeight(.bold)
Text("tasks completed")
.foregroundStyle(.secondary)
}
Button {
count += 1
} label: {
Text("Complete +1")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding()
.background(.blue)
.foregroundStyle(.white)
.clipShape(
RoundedRectangle(cornerRadius: 14)
)
}
VStack(alignment: .leading, spacing: 8) {
Text("Your Name")
.font(.headline)
TextField("Enter your name", text: $name)
.textFieldStyle(.roundedBorder)
}
Toggle(
"Daily Reminder",
isOn: $reminderEnabled
)
if count >= 5 {
Text("π Daily goal completed!")
.font(.headline)
}
Spacer()
Button("Reset") {
count = 0
}
}
.padding()
}
}
There's quite a bit of code here, but look at the state first:
@State private var count = 0
@State private var name = ""
@State private var reminderEnabled = false
That's our screen's data.
Then look at the UI:
count β Counter name β TextField reminderEnabled β Toggle
And finally the user changes that data:
Button βββββββ count TextField ββββ name Toggle βββββββ reminderEnabled
That's the important part of this lessonβnot memorizing the syntax.
One More Important Idea: State Has an Owner
We wrote:
struct DailyProgressView: View {
@State private var count = 0
}
That means:
DailyProgressViewownscount.
This will become important later.
For now, a useful beginner rule is:
Use @State when a view owns simple UI state.
Good examples include:
@State private var count = 0
@State private var isShowingDetails = false
@State private var searchText = ""
@State private var isEnabled = true
Don't worry yet about all the other SwiftUI property wrappers you've probably seen:
@Binding @Environment @Observable @StateObject @ObservedObject
We'll introduce them when there's a reason to use them.
Learning all of them at once usually makes SwiftUI unnecessarily confusing.
Lesson 3 Mental Model
If you remember only one diagram from this lesson, remember this:
βββββββββββββββ
β STATE β
β β
β count = 3 β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β UI β
β β
β Count: 3 β
βββββββββββββββ
β²
β
User Interaction
β
βββββββββββββββ
β Button β
βββββββββββββββ
The user interacts with the interface.
That changes state.
SwiftUI uses the new state to update the interface.
UIKit thinking
Button tapped
β
Find UI element
β
Change its properties
SwiftUI thinking
Button tapped
β
Change state
β
UI reflects state
That shift in thinking is much more important than memorizing individual SwiftUI APIs.
Your Challenge
Build a tiny Water Tracker:
π§ Water Tracker 3 glasses today [ + Add Glass ] Goal: 8 glasses Notifications ON [ Reset ]
Your state might start with:
@State private var glasses = 0
@State private var notificationsEnabled = false
Requirements:
- Add Glass increases
glasses. - Reset changes it back to
0. - A
Togglecontrols notifications. - When
glasses >= 8, show:
π Daily goal reached!
Bonus challenge
Don't let the counter go higher than 8.
Think about what could go inside:
Button {
// What condition could go here?
}
You already know enough Swift to solve that. π
What You Now Know
After only three lessons, you can already understand the foundations of a surprising amount of SwiftUI code:
Text
Image
VStack
HStack
ZStack
Spacer
.padding()
.frame()
.background()
.foregroundStyle()
Button
@State
TextField
Toggle
if
More importantly, you now understand the fundamental relationship:
State β UI
Our next lesson should not be another pile of small controls.
Lesson 4 β Working With Lists of Data
Instead of hardcoding:
Text("Apple")
Text("Banana")
Text("Orange")
we'll learn how real apps represent data using Swift models, then turn collections of that data into UI using ForEach, List, and Identifiable.
That's where SwiftUI starts feeling much more like building a real application.