This lesson continues Object Types: Building Things with State and Behavior. There, a Pet struct stored information and performed work. Now we will use the same Swift ideas to describe an interface.

How do familiar Swift types become an interface that changes when the user acts?

SwiftUI adds new tools, but it does not replace Swift. Views are values created from types. They have properties, use initializers, call methods, and receive closures. The new part is how those pieces describe what should appear on screen.

Views Are Swift Values

Q1: What is familiar about Text("Hello")?

Compare these expressions:

Text("Hello")
Pet(name: "Cookie", energy: 5)

Which names identify types? What do the complete expressions produce, and what familiar Swift feature do the parentheses suggest?

Show answer Hide answer

Text and Pet are type names. Each complete expression calls an initializer and produces a value of that type:

Pet(...)  → a Pet value
Text(...) → a Text value

The string "Hello" supplies the information needed to initialize the Text. SwiftUI is already using the same relationship between types, initializers, and values that we used to create pets.

Q2: What does a custom view declaration promise?

Read this code line by line:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello")
    }
}

Identify the familiar struct declaration and the property. Then decide what : View and some View must mean well enough for now.

Show answer Hide answer

ContentView is a struct, and body is a computed property. It is a property rather than a method because its name is not followed by parentheses.

struct ContentView: View

The colon says that ContentView conforms to the SwiftUI View protocol. In plain language, the type promises to meet SwiftUI’s requirements for a view.

var body: some View

The body property produces one specific kind of value that conforms to View. The keyword some lets the implementation keep that exact type out of the declaration. At this stage, the useful reading is: the body produces a view.

Q3: How is ContentView like Pet?

Compare:

struct Pet {
    var name: String
}
struct ContentView: View {
    var body: some View {
        Text("Hello")
    }
}

What do these types share, and what different job does each perform?

Show answer Hide answer

Both are structs with properties. Pet has a stored property named name; ContentView has a computed property named body.

Their purposes differ:

Pet          models a thing
ContentView  describes an interface

A SwiftUI view is not a separate species of programming construct. It is a Swift type that satisfies the rules of the View protocol.

Compose an Interface

One Text value is not much of an interface. SwiftUI builds larger views by combining smaller ones.

Q4: What layout will VStack produce?

Predict the arrangement before running the code:

VStack {
    Text("Cookie")
    Text("Energy: 5")
    Text("Hungry")
}

Will the three values appear across one row, down one column, or will only the first appear?

Show answer Hide answer

They appear in a vertical column:

Cookie
Energy: 5
Hungry

VStack is both a view and a container for other views. It arranges its children vertically. This is view composition: a larger interface is built from smaller views instead of being written as one giant object.

Q5: What do view modifiers actually do?

Use what you know about dot notation to read this expression:

Text("Cookie")
    .font(.title)
    .padding()

What does the expression begin with? What does each chained call affect? Does either call change the string "Cookie"?

Show answer Hide answer

The expression begins by creating a Text view. .font(.title) describes a title-sized font, and .padding() adds space around the result.

The string remains "Cookie". More precisely, a SwiftUI modifier returns a new view that includes the requested change. It does not reach into the original Text value and mutate it.

create Text → return a view with a title font → return a view with padding

The calls look like ordinary methods because they are methods. SwiftUI uses familiar Swift syntax to construct a description of the interface.

Q6: Which container matches the intended layout?

This code compiles, but it places the labels beside each other:

struct ContentView: View {
    var body: some View {
        HStack {
            Text("Cookie")
            Text("Energy: 5")
        }
    }
}

Change it so the energy appears below the name. What does your repair tell SwiftUI?

Show answer Hide answer

Replace HStack with VStack:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Cookie")
            Text("Energy: 5")
        }
    }
}

HStack arranges its children horizontally. VStack arranges them vertically. Choosing a container is part of describing the relationship among views, not merely a way to make the compiler accept several expressions.

Buttons Run Closures

An interface becomes useful when it can respond to someone.

Q7: When does a button’s code run?

Button("Feed") {
    print("Feeding pet")
}

What appears on screen? When does the print statement execute, and what familiar Swift concept appears between the braces?

Show answer Hide answer

The user sees a button labeled Feed. The print statement runs when the button is pressed, not when Swift creates the button.

The braces contain a closure: a function value passed to Button so SwiftUI can call it later. This form is called trailing-closure syntax because the closure appears after the initializer’s parentheses.

The button therefore combines two different pieces of information:

"Feed"                  what the button displays
{ print("Feeding pet") } what the button does

State Connects Data to the Screen

Printing proves that a button ran, but it does not change the visible energy. The number on screen must depend on a value that can change.

Q8: What changes after each press?

Predict the initial display and the result after four button presses:

struct ContentView: View {
    @State private var energy = 5

    var body: some View {
        VStack {
            Text("Energy: \(energy)")

            Button("Feed") {
                energy += 1
            }
        }
    }
}

Which line changes the state, and which line displays it?

Show answer Hide answer

The interface begins with Energy: 5. One press changes it to Energy: 6; four presses change it to Energy: 9.

This line changes the state:

energy += 1

This line reads that state while describing the interface:

Text("Energy: \(energy)")

When energy changes, SwiftUI reevaluates the view’s body and updates the affected display. You describe the relationship between state and interface; SwiftUI performs the update.

Q9: Why isn’t an ordinary property enough?

A first attempt might look like this:

struct ContentView: View {
    let energy = 5

    var body: some View {
        Button("Feed") {
            energy += 1
        }
    }
}

Why does this fail? Why is changing let to an ordinary var still not the right model for local interface state?

Show answer Hide answer

let creates a constant, so energy += 1 cannot compile.

An ordinary mutable property is also a poor fit. SwiftUI view structs are temporary descriptions that the framework may create again as the interface changes. Local state needs storage with a lifetime managed by SwiftUI:

@State private var energy = 5

@State tells SwiftUI to preserve this value for the view’s identity and to update the interface when it changes. private keeps the storage as an implementation detail of ContentView.

Read the Whole View

Q10: How should the complete view protect its state?

Read the complete view before opening the answer:

import SwiftUI

struct PetView: View {
    let name: String
    @State private var energy = 5

    init(name: String = "Cookie") {
        self.name = name
    }

    var body: some View {
        VStack(spacing: 12) {
            Text(name)
                .font(.title)

            Text("Energy: \(energy)")

            HStack {
                Button("Feed") {
                    energy = min(energy + 1, 10)
                }
                .disabled(energy >= 10)

                Button("Play") {
                    energy = max(energy - 1, 0)
                }
                .disabled(energy <= 0)
            }
        }
        .padding()
    }
}

Identify the custom type, its initializer, its three properties, the views composed inside body, and the two closures. Why can the energy no longer become negative? What does PetView() display if its caller supplies no name?

Show answer Hide answer

PetView is a struct that conforms to View. It has three properties with different roles:

let name: String                // fixed information supplied from outside
@State private var energy = 5  // local state with a default value
var body: some View             // the interface description

The initializer also gives name a default without making it mutable:

init(name: String = "Cookie") {
    self.name = name
}

Both PetView() and PetView(name: "Mochi") are now valid. The first displays Cookie; the second displays Mochi. Energy begins at 5 in either view.

The body composes VStack, HStack, Text, and Button views. .font(.title), .padding(), and .disabled(...) are modifiers. Each button receives a closure. Feed raises energy but caps it at 10; Play lowers energy but floors it at 0.

The assignments defend the 0...10 rule. The disabled states also show the user when an action has reached its limit. Keeping both matters: the interface communicates the boundary, and the state-changing code still protects it.

Both labels read properties. Text(name) displays fixed input, while Text("Energy: \(energy)") displays changing state. When a closure changes energy, SwiftUI reevaluates the body and refreshes the energy label.

The bridge from the previous lesson is direct:

struct and protocol conformance → PetView: View
properties                      → name, energy, body
initializers                    → PetView(...), Text(...), Button(...)
closures                        → button actions
dot notation                    → view modifiers
state and its valid range       → @State, min(...), max(...)

Practice: Extend the Pet View

Q11: Can Sleep obey the same energy rule?

Add a Sleep button that restores 2 energy. It must never raise energy above 10, and it should be disabled when the pet is already fully rested. Keep all three buttons in the same HStack.

Starting from 5, predict the result of pressing Play, Sleep, Sleep, Sleep before you run the code.

Show answer Hide answer

The sequence produces 4, 6, 8, then 10. The last increase stops at the upper boundary.

Button("Sleep") {
    energy = min(energy + 2, 10)
}
.disabled(energy >= 10)

This is the same rule used by Feed, but the change is larger. If energy were 9, Sleep would still finish at 10, not 11.

Q12: Should mood be another piece of state?

Display Tired when energy is 0...2, Ready when it is 3...7, and Energetic when it is 8...10.

Do not add another @State property. Derive the mood from energy so it can never disagree with the number on screen.

Show answer Hide answer

A computed property keeps one source of truth:

var mood: String {
    if energy <= 2 {
        return "Tired"
    }

    if energy >= 8 {
        return "Energetic"
    }

    return "Ready"
}

Then place this view beside the energy label:

Text("Mood: \(mood)")

Mood changes whenever SwiftUI reevaluates body. A second stored state value would create an avoidable bug: one button might change energy but forget to change mood.

Q13: Can you learn one unfamiliar view from its documentation?

Replace the plain energy display with a determinate ProgressView that runs from 0 to 10. Keep the exact number visible as well.

Begin with Apple’s ProgressView documentation. Find an initializer that accepts a current value and a total. Determine why Double(energy) is useful even though the model stores an Int.

Show answer Hide answer

One compact solution is:

ProgressView("Energy", value: Double(energy), total: 10)

Text("\(energy) / 10")

This is a determinate progress view because it receives both the present value and the total. Its numeric initializer expects a floating-point value, so Double(energy) converts the Int without changing the stored state.

Do not stop after the code compiles. Press each button at both boundaries and confirm that the bar, number, disabled buttons, and mood all describe the same state.

Prepare for the Next Lesson

PetView currently owns its energy and protects the valid range itself. That is enough for one small view, but larger interfaces raise new questions. What if a parent view owns the pet? How can a smaller child view display that value? How can the child change state it does not own? Should every screen repeat the 0...10 rule, or should the model enforce it once?

Those questions lead to SwiftUI data flow: passing values into views, separating model rules from presentation, and using bindings when one view needs to edit state owned by another.

Continue to Part 2: Data Flow and Bindings →