SwiftUI.cc
Snippets
ControlsValue InputBeginner

SwiftUI Stepper Range and Custom Labels

Stepper increments a value in precise steps with plus/minus buttons. Clamp with a range, format the label live, or supply custom onIncrement/onDecrement logic.

3 min readUpdated 2026-06
Ticket quantity with bounds
import SwiftUI

struct TicketPicker: View {
    @State private var tickets = 2

    var body: some View {
        Form {
            Stepper(value: $tickets, in: 1...8) {
                LabeledContent("Tickets", value: "\(tickets)")
            }
            Stepper("Font size", onIncrement: { /* +1 */ },
                    onDecrement: { /* -1 */ })
        }
    }
}

Stepper preview

Precise, bounded, deliberate

Where a slider says "about this much," a stepper says "exactly seven." Stepper(value: $count, in: 1...8, step: 1) clamps automatically and grays out the minus button at 1 and the plus at 8 — boundary handling for free.

Show the number

The stepper renders only the buttons and your label, so the label must carry the value. LabeledContent("Tickets", value: "\(tickets)") inside the label slot is the cleanest form-row presentation, keeping the number aligned with other rows' values.

Closure-driven steps

Stepper("Zoom") {
    zoom = min(zoom * 1.25, 8)     // onIncrement
} onDecrement: {
    zoom = max(zoom / 1.25, 0.25)
}

The closure form supports non-linear steps (multiplicative zoom), cross-field validation, or analytics per tap. Pass nil for a direction to disable it dynamically.

Hold to repeat

Steppers auto-repeat while held, accelerating over time — which is why bounds matter even for "obviously small" values.

Common mistakes

  • Hiding the value somewhere far from the control.
  • Using steppers for ranges that need dozens of taps to traverse.
  • Side effects in body instead of the step closures, firing on unrelated renders.

Related reference