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:

"count is 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 count is at least 5, this Text should 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:

DailyProgressView owns count.

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

Your turn

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 Toggle controls 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.