```html SwiftUI: How to Implement Slider Input (iOS 17+, 2026)

How to Implement Slider Input in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: Slider Updated: May 11, 2026
TL;DR

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

  1. @State private var volume: Double = 0.7 — Each slider owns a @State Double that SwiftUI observes. When the user drags the thumb, SwiftUI writes the new value back through the $volume binding and re-renders the label automatically.
  2. Slider(value:in:step:) — The in: parameter is a ClosedRange<Double> that clamps user input. The optional step: parameter snaps the thumb to discrete increments — 0.25 for playback speed means values jump 0.5 → 0.75 → 1.0 and so on.
  3. Minimum and maximum value labels — The minimumValueLabel: and maximumValueLabel: trailing closures accept any View. Using SF Symbol images for volume and plain Text for dB/speed gives each slider a contextual anchor without extra layout code.
  4. .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.
  5. .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

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.

```