How to Implement Slider Input in SwiftUI
Use SwiftUI's Slider(value:in:) with a @State binding to let users pick a value within a range. Pass a step: parameter to snap to discrete increments, and add a label closure for VoiceOver support.
struct QuickSlider: View {
@State private var volume: Double = 0.5
var body: some View {
VStack {
Text("Volume: \(volume, specifier: "%.2f")")
Slider(value: $volume, in: 0...1) {
Text("Volume")
}
.accessibilityLabel("Volume slider")
}
.padding()
}
}
Full implementation
The example below builds a polished audio-settings form with three sliders — volume, bass, and playback speed — each with a live value readout, a step interval, and minimum/maximum value labels. The sliders are wrapped in a Form so they match the iOS Settings aesthetic out of the box. Every slider exposes an accessibilityValue so VoiceOver announces the current setting precisely.
import SwiftUI
struct AudioSettingsForm: View {
@State private var volume: Double = 0.7
@State private var bass: Double = 0.5
@State private var playbackSpeed: Double = 1.0
// Formatter for speed display
private var speedText: String {
String(format: "%.1fx", playbackSpeed)
}
var body: some View {
NavigationStack {
Form {
// MARK: - Volume
Section("Volume") {
VStack(alignment: .leading, spacing: 6) {
HStack {
Label("Volume", systemImage: "speaker.wave.2")
Spacer()
Text("\(Int(volume * 100))%")
.monospacedDigit()
.foregroundStyle(.secondary)
}
Slider(
value: $volume,
in: 0...1,
step: 0.01
) {
Text("Volume")
} minimumValueLabel: {
Image(systemName: "speaker")
.foregroundStyle(.secondary)
} maximumValueLabel: {
Image(systemName: "speaker.wave.3")
.foregroundStyle(.secondary)
}
.tint(.blue)
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(volume * 100)) percent")
}
.padding(.vertical, 4)
}
// MARK: - Bass
Section("Equalizer") {
VStack(alignment: .leading, spacing: 6) {
HStack {
Label("Bass", systemImage: "waveform.path")
Spacer()
Text("\(Int((bass - 0.5) * 20), specifier: "%+d") dB")
.monospacedDigit()
.foregroundStyle(.secondary)
}
Slider(
value: $bass,
in: 0...1,
step: 0.05
) {
Text("Bass")
} minimumValueLabel: {
Text("-10")
.font(.caption2)
.foregroundStyle(.secondary)
} maximumValueLabel: {
Text("+10")
.font(.caption2)
.foregroundStyle(.secondary)
}
.tint(.purple)
.accessibilityLabel("Bass boost")
.accessibilityValue("\(Int((bass - 0.5) * 20)) decibels")
}
.padding(.vertical, 4)
}
// MARK: - Playback Speed
Section("Playback") {
VStack(alignment: .leading, spacing: 6) {
HStack {
Label("Speed", systemImage: "gauge.with.dots.needle.67percent")
Spacer()
Text(speedText)
.monospacedDigit()
.foregroundStyle(.secondary)
}
Slider(
value: $playbackSpeed,
in: 0.5...3.0,
step: 0.25
) {
Text("Playback Speed")
} minimumValueLabel: {
Text("0.5×")
.font(.caption2)
.foregroundStyle(.secondary)
} maximumValueLabel: {
Text("3×")
.font(.caption2)
.foregroundStyle(.secondary)
}
.tint(.orange)
.accessibilityLabel("Playback speed")
.accessibilityValue(speedText)
}
.padding(.vertical, 4)
}
// MARK: - Reset
Section {
Button("Reset to Defaults", role: .destructive) {
withAnimation {
volume = 0.7
bass = 0.5
playbackSpeed = 1.0
}
}
}
}
.navigationTitle("Audio Settings")
}
}
}
#Preview {
AudioSettingsForm()
}
How it works
-
@State private var volume: Double = 0.7— Each slider owns a@StateDouble that SwiftUI observes. When the user drags the thumb, SwiftUI writes the new value back through the$volumebinding and re-renders the label automatically. -
Slider(value:in:step:)— Thein:parameter is aClosedRange<Double>that clamps user input. The optionalstep:parameter snaps the thumb to discrete increments —0.25for playback speed means values jump 0.5 → 0.75 → 1.0 and so on. -
Minimum and maximum value labels — The
minimumValueLabel:andmaximumValueLabel:trailing closures accept anyView. Using SF Symbol images for volume and plainTextfor dB/speed gives each slider a contextual anchor without extra layout code. -
.tint(.blue)— Applied to the slider to color the filled track portion. Each slider gets a distinct tint so users can scan the form at a glance and quickly identify each control. -
.accessibilityLabel+.accessibilityValue— VoiceOver reads the label first ("Volume") then the dynamic value ("70 percent"). Without.accessibilityValue, VoiceOver would announce the raw Double, which is hard to parse — always override it with a formatted string.
Variants
Continuous callback with onEditingChanged
Use the onEditingChanged: closure to fire an action only when the user lifts their finger — useful for expensive operations like fetching filtered results.
struct PriceRangeSlider: View {
@State private var maxPrice: Double = 500
@State private var isFetching = false
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Max price: \(maxPrice, format: .currency(code: "USD"))")
.fontWeight(.medium)
Slider(
value: $maxPrice,
in: 0...2000,
step: 50
) {
Text("Max Price")
} minimumValueLabel: {
Text("$0").font(.caption2)
} maximumValueLabel: {
Text("$2k").font(.caption2)
} onEditingChanged: { editing in
// editing == false means the thumb was released
if !editing {
isFetching = true
Task {
await fetchProducts(upTo: maxPrice)
isFetching = false
}
}
}
if isFetching {
ProgressView("Updating results…")
.font(.caption)
}
}
.padding()
}
func fetchProducts(upTo price: Double) async {
try? await Task.sleep(for: .milliseconds(600))
}
}
Vertical slider using .rotationEffect
SwiftUI's Slider is always horizontal by default. To create a vertical slider — common in mixer UIs — apply .rotationEffect(.degrees(-90)) and constrain the frame:
Slider(value: $level, in: 0...1)
.rotationEffect(.degrees(-90))
.frame(width: 200) // becomes the height after rotation
.accessibilityLabel("Channel level")
Common pitfalls
-
🔶 iOS version: The five-argument
Slider(value:in:step:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)initialiser with all trailing closures requires iOS 13.4+, but thestep:parameter with label closures works cleanly from iOS 17. Avoid the deprecatedaccentColormodifier — use.tint()instead. -
🔶 Binding type mismatch:
Sliderrequires aBinding<Double>(orFloat/BinaryFloatingPoint). If your model stores anInt, wrap it:Binding<Double>(get: { Double(count) }, set: { count = Int($0) }). -
🔶 Missing
.accessibilityValue: Without it VoiceOver reads the raw floating-point number, e.g. "0.699999988" instead of "70 percent". Always supply a formatted.accessibilityValuestring — this is one of the most common accessibility bugs in form screens. -
🔶 Heavy work in the binding setter: The binding setter fires on every frame while the thumb is dragged — potentially 60 times per second. Defer expensive operations (network calls, Core Data writes) to
onEditingChangedso the drag stays smooth.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement slider input in SwiftUI for iOS 17+. Use Slider(value:in:step:) with a @State Double binding. Include minimumValueLabel and maximumValueLabel closures. Apply .tint() for the track color. Make it accessible (VoiceOver labels and .accessibilityValue with a formatted string). Add a #Preview with realistic sample data (e.g. an audio settings form).
Drop this prompt into Soarias during the Build phase after your screens are scaffolded — it will wire the slider directly into your existing form view with correct state management and no extra boilerplate.
Related
FAQ
Does this work on iOS 16?
Yes — the core Slider(value:in:step:) API and the label/minimumValueLabel/maximumValueLabel closures are available back to iOS 13. However, some tint rendering improvements and form integration polish landed in iOS 17. If you need iOS 16 support, the code here compiles without changes; just set your deployment target accordingly and test that .tint() renders as expected.
How do I bind a Slider to an integer value?
Slider requires a floating-point binding. Create a computed Binding<Double> that converts on the fly:
Slider(value: Binding(get: { Double(count) }, set: { count = Int($0) }), in: 1...10, step: 1)
Or keep the backing store as @State private var count: Double and cast to Int only when reading.
What's the UIKit equivalent of SwiftUI's Slider?
UISlider is the UIKit counterpart. You set minimumValue, maximumValue, and value properties, then add a target for the .valueChanged control event. SwiftUI's Slider replaces all of that with a single declarative call and automatic two-way binding — no target/action wiring needed. If you need a UISlider inside SwiftUI for legacy reasons, wrap it with UIViewRepresentable.
Last reviewed: 2026-05-11 by the Soarias team.