Buttons and User Input in SwiftUI
SwiftUI provides a family of interactive controls—Button, Toggle, Slider, Stepper, TextField, and TextEditor—that let users generate events which your app responds to by mutating state. Unlike UIKit, where you wire up target-action pairs or delegate callbacks, SwiftUI controls take a closure or a binding directly in their initializer. When the user taps, drags, or types, SwiftUI runs your closure (for stateless actions) or writes into the bound value (for stateful controls), and because SwiftUI views are a function of state, any dependent view automatically re-renders. This closes the loop between input and output without manual view-refresh code.
Cricket analogy: A batsman's shot (tap) sends the ball toward the boundary, and the scoreboard updates itself automatically because the whole stadium display is wired to the run count, not because someone manually chalks a new number on a board.
The Button View
A Button is initialized with a label (text, an Image, or an arbitrary view builder) and an action closure that runs on tap. Buttons are lightweight: they don't own state themselves, they merely invoke a closure, so all the state they affect must live in an enclosing view or a model object. Button styles—.borderedProminent, .bordered, .plain—change appearance without changing behavior, and role: .destructive or role: .cancel communicate semantic intent to assistive technologies and to system-provided styling.
Cricket analogy: A captain calling for a bowling change is like tapping a Button: the closure (bringing on the spinner) runs, but the captain doesn't personally hold the match state—the scoreboard and field settings owned elsewhere update in response.
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Count: \(count)")
.font(.title2)
HStack {
Button("Decrement", role: .destructive) {
count -= 1
}
.buttonStyle(.bordered)
Button("Increment") {
count += 1
}
.buttonStyle(.borderedProminent)
}
}
.padding()
}
}Bound Controls: Toggle, Slider, Stepper, TextField
Controls like Toggle, Slider, Stepper, and TextField don't take an action closure—they take a Binding to a piece of state and read/write it directly as the user interacts. A Toggle binds to a Bool, a Slider and Stepper bind to a numeric value (often with a in: range and, for Stepper, a step:), and a TextField binds to a String. Because the binding is two-way, the control both displays the current value and updates it, which means the source of truth always lives in exactly one place—the @State (or model property) that owns the binding, referenced with the $ prefix.
Cricket analogy: A Duckworth-Lewis target display is two-way bound to overs remaining: change the overs and the target recalculates live, just like a Slider bound to a value keeps the displayed number and the underlying state perfectly in sync.
struct PreferencesForm: View {
@State private var username = ""
@State private var notificationsEnabled = true
@State private var volume: Double = 50
var body: some View {
Form {
TextField("Username", text: $username)
.textInputAutocapitalization(.never)
Toggle("Enable notifications", isOn: $notificationsEnabled)
Slider(value: $volume, in: 0...100, step: 1) {
Text("Volume")
}
}
}
}Think of a binding as a two-way pipe, not a copy: $username doesn't hand TextField a snapshot of the string, it hands it read/write access to the exact storage cell that owns username. That's why typing in the field is reflected instantly wherever username is read elsewhere in the view.
A common mistake is calling a mutating action directly inside body (e.g. count += 1 outside of a closure). Any state mutation must happen inside an event closure like a button action, .onChange, or .onAppear—mutating state during body evaluation causes undefined behavior and SwiftUI will warn or crash at runtime.
Keyboard, Focus, and Submission
TextField integrates with the keyboard through modifiers like .keyboardType(.emailAddress), .textContentType(.password), and .submitLabel(.done). The @FocusState property wrapper lets you programmatically move focus between fields (useful for 'next field' navigation) and .onSubmit runs a closure when the user presses return or the keyboard's submit button, which is the idiomatic way to trigger a form submission without a separate button tap.
Cricket analogy: Just as a scorer's app switches to a numeric keypad specifically for entering run totals, a TextField's .keyboardType(.numberPad) presents the right input layout for the data being entered, and .onSubmit is like the scorer pressing 'confirm' to lock in the over's total.
- Button takes an action closure; Toggle, Slider, Stepper, and TextField take a two-way Binding instead.
- Use the
$prefix to pass a binding from@State(e.g.$username) into a control. - buttonStyle and role (e.g.
.destructive) control appearance and communicate semantic intent. - State must only be mutated inside closures (actions,
.onChange,.onSubmit)—never during body evaluation. @FocusStateprogrammatically manages keyboard focus across multiple input fields.- Because views are a function of state, any control update automatically re-renders dependent views.
Practice what you learned
1. How does a SwiftUI Button primarily communicate a user interaction to your code?
2. What type of parameter does a Slider require to reflect and update its value?
3. Where is it safe to mutate @State such as `count += 1`?
4. What does @FocusState primarily manage?
5. Why is `role: .destructive` used on a Button?
Was this page helpful?
You May Also Like
@State and @Binding
Understand how @State owns local view state and how @Binding lets child views read and write that state without owning it themselves.
Text and Typography in SwiftUI
How the Text view handles fonts, Dynamic Type, styling, and localization to produce accessible, readable typography.
Modifiers in SwiftUI
How view modifiers work under the hood, why order matters, and how chained modifiers build up new wrapped view types.
Views and the View Protocol
How the `View` protocol works, what `body` must return, and how SwiftUI composes small views into larger interfaces.