SwiftUI State and Binding: @State, Button and TextField

iOS App Development with Swift and SwiftUI: Beginner to Job-Ready Projects · SwiftUI · 6 min read · Updated

State drives the screen

  1. The user taps a button or types in a field.
  2. Your code changes a @State value. You only change data, never the label.
  3. SwiftUI notices that a value used by the view has changed.
  4. The view body runs again and the screen shows the new value.

A screen that never changes is only a picture. To react to taps and typing, SwiftUI needs a place to keep changing data. That place is state. The rule of SwiftUI is simple and strict: *when state changes, the views that read it are drawn again*. You never say *update this label*. You only change the data.

@State

A view is a struct, and structs are values that SwiftUI creates and throws away often. So an ordinary var inside a view would be lost. Putting @State in front of a property tells SwiftUI to keep that value safely for the life of the view and to redraw the view when it changes. By habit, state is private, because the view that owns it is the one that changes it.

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("Taps: \(count)")
                .font(.title)

            Button("Add one") {
                count += 1
            }
            .buttonStyle(.borderedProminent)

            Button("Reset", role: .destructive) {
                count = 0
            }
        }
        .padding()
    }
}

The closure after Button("Add one") is the code that runs on a tap. It changes count. SwiftUI sees the change and draws the Text again with the new number. There is no other step. Notice role: .destructive, which makes the button red, because a button that throws things away should look different.

The dollar sign: bindings

A text field must both *show* a value and *change* it when the user types. A plain value cannot do the second job. A binding is a two-way connection to a value that lives somewhere else. You make one from a state property by putting a dollar sign in front of its name.

struct AddExpenseForm: View {
    @State private var title = ""
    @State private var amountText = ""
    @State private var isRecurring = false

    var body: some View {
        VStack(spacing: 12) {
            TextField("What did you buy?", text: $title)
                .textFieldStyle(.roundedBorder)

            TextField("Amount", text: $amountText)
                .textFieldStyle(.roundedBorder)
                .keyboardType(.decimalPad)

            Toggle("Repeats every month", isOn: $isRecurring)

            Text("You typed: \(title)")
                .foregroundStyle(.secondary)
        }
        .padding()
    }
}
  • title (no dollar sign) is the value. Use it to *read*.
  • $title is a binding. Use it to let another view *read and write* the same value, as TextField and Toggle do.
  • As you type, the text field writes to title, SwiftUI notices, and the Text below updates on each key press.

@Binding: sharing state with a child view

When you split a form into smaller views, the child needs to change the parent's data. Give the child a @Binding property. The parent passes $value, and the child reads and writes it, without owning it. There is still only one real copy of the data, which is why the parent and child can never disagree.

struct AmountField: View {
    @Binding var text: String

    var body: some View {
        HStack {
            Text("$")
            TextField("0.00", text: $text)
                .keyboardType(.decimalPad)
        }
        .padding(10)
        .background(.quaternary, in: RoundedRectangle(cornerRadius: 10))
    }
}

struct ParentView: View {
    @State private var amountText = ""

    var body: some View {
        VStack {
            AmountField(text: $amountText)
            Text("Value: \(amountText)")
        }
        .padding()
    }
}

Turning text into a number

A text field holds a String, but an amount is a Double. Lesson 5 gave you the tool: Double(amountText) returns an optional. Use it to decide whether the form is valid, and disable the button when it is not.

private var amount: Double? { Double(amountText) }
private var isValid: Bool {
    !title.trimmingCharacters(in: .whitespaces).isEmpty && (amount ?? 0) > 0
}

// in body:
Button("Add expense") {
    // save it
}
.disabled(!isValid)

What should be state?

  • Data that changes and that the screen shows: a counter, a text field's text, whether a sheet is open.
  • Not data that can be worked out from other state. A total is a *computed property*, not a second state value. Two copies of one fact can go out of step.
  • Keep state as low as you can and as local as you can. If two views need to change it, put it in their nearest shared parent and pass a binding.

Common mistakes

  • Changing a var property that has no @State. The screen does not update, and Xcode may refuse to compile it.
  • Forgetting the dollar sign in TextField(text: title). The error says it expected a Binding<String>.
  • Putting state in a child view when the parent also needs it.

Your task

  1. Build CounterView and add a button that subtracts one but never goes below zero.
  2. Build the form with title, amount and a toggle, and show a live summary line under it.
  3. Move the amount field into its own view with @Binding.
  4. Disable the Add button until the title is not empty and the amount is above zero.

How to check it worked

  • Typing in the field updates the summary line on every key press.
  • The button turns on only when the form is valid.
  • You can say what @State, $title and @Binding each do.

Frequently asked questions

What does @State do in SwiftUI?

It tells SwiftUI to keep a value safely for the life of the view and to redraw the view whenever the value changes. State is usually private to the view that owns it.

What does the dollar sign mean in $title?

It makes a binding, a two-way link to the value, so a control such as a TextField can both show it and change it.

When do I use @Binding?

When a child view needs to change data that is owned by its parent. The parent passes $value and the child declares @Binding, so there is still only one copy of the data.