SwiftUI: Discardable Slider
Slider wrapper to prevent extra changes
While starting a new project, feature, or screen there is always a good chance to make it great in terms of functionality, architecture and UI right from the start. This sets the mood for further work and make us happy in the long run. Especially, when this approach will help to develop a robust and efficient components without profiling the entire application.
Our Task
We need to make a
Sliderwhich will not instantly change the initial value, but only after some sort of confirmation. Why: slider triggers a change of event on each interaction and this change is linked to multiple events in Data Layer and Network layer. For example: each change might go to SwiftData/CoreData and re-trigger the change of other components which is redundant and not needed.
Let’s start building it step by step! Or tick by tick) It’s a Slider after all.
Slider in SwiftUI is pretty straightforward component. From the first look. Let’s make a range from 0 to 10 with step 1 to pick the values in slider. Keep in mind, that it accepts only values conforming to BinaryFloatingPoint:
@State private var filterValue = 5.0
VStack {
Text(”Values to filter”)
Slider(value: $filterValue, in: 0...10, step: 1)
}.onChange(of: filterValue) {
print(”Changed to \(filterValue)”)
}Looks good, but let’s add some ticks? There is such feature:
Slider(value: $filterValue, in: 0...10, step: 1, label: {}, tick: {
value in
SliderTick(value, label: {
Text(String(format: “%.0f”, value))
})
})Unfortunately, this will work only for macOS. To add some basic automatic tick you need to pass label. It can be Label or EmptyView.
VStack {
Text(”Values to filter”)
//Small change, yeah?
Slider(value: $filterValue, in: 0...10, step: 1) {}
}.onChange(of: filterValue) {
print(”Changed to \(filterValue)”)
}Now we can see that our onChange is triggered all the time we change value. Let’s wrap this Slider into another View. Let’s call it DiscardableSlider:
struct DiscardableSlider<V: Strideable>: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
internal init(value: Binding<V>, bounds: ClosedRange<V>, step: V.Stride) {
self._value = value
self.bounds = bounds
self.step = step
self._pendingValue = State(initialValue: value.wrappedValue)
}
//Same parameters as native Slider
@Binding var value: V
let bounds: ClosedRange<V>
let step: V.Stride
//Local value for inner state
@State private var pendingValue: V
var body: some View {
VStack(spacing: 4.0) {
Slider(value: $pendingValue, in: bounds, step: step) {}
}
}
}To make slider discardable we can add button below it. You might use .overlay to prevent size growth. In my case, height change is needed so putting the controls below the slider. I’m still experimenting with Liquid Glass (as all developers probably) and placed it inside a GlassEffectContainer.
struct DiscardableSliderStep1<V: Strideable>: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
internal init(value: Binding<V>, bounds: ClosedRange<V>, step: V.Stride) {
self._value = value
self.bounds = bounds
self.step = step
self._pendingValue = State(initialValue: value.wrappedValue)
}
//Same parameters as native Slider
@Binding var value: V
let bounds: ClosedRange<V>
let step: V.Stride
//Local value for inner state
@State private var pendingValue: V
var body: some View {
VStack(spacing: 4.0) {
Slider(value: $pendingValue, in: bounds, step: step) {}
//Discard panel
HStack {
Spacer()
buttonsStack
.padding(4)
}
}
}
private var buttonsStack: some View {
GlassEffectContainer {
HStack(spacing: 12.0) {
Button {
withAnimation {
pendingValue = value
}
} label: {
Label(”Cancel”, systemImage: “xmark.circle”)
.labelStyle(.iconOnly) // compact; keep just the icon if you prefer
.foregroundStyle(.red)
}
.frame(width: 40, height: 40)
.buttonStyle(.glass)
Button {
value = pendingValue
} label: {
Label(”Save”, systemImage: “checkmark.circle.fill”)
.labelStyle(.iconOnly)
.font(.title3)
.foregroundStyle(.green) // green checkmark
}
.frame(width: 40, height: 40)
.buttonStyle(.glass)
}
}
}
}What do we have here:
Cancel button to discard slider value with animation
Apply button to trigger update of initial value
Right now it’s visible all the time. Do we need to show it if the value hasn’t changed? Of course, no. By adding local animation variable which will trigger the panel or hide it after action.
struct DiscardableSlider<V: Strideable>: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
internal init(value: Binding<V>, bounds: ClosedRange<V>, step: V.Stride) {
self._value = value
self.bounds = bounds
self.step = step
self._pendingValue = State(initialValue: value.wrappedValue)
}
@Binding var value: V
let bounds: ClosedRange<V>
let step: V.Stride
@State private var pendingValue: V
@State private var showDiscard: Bool = false
var body: some View {
VStack(spacing: 4.0) {
Slider(value: $pendingValue, in: bounds, step: step) {}
.onChange(of: pendingValue) {
withAnimation {
showDiscard = pendingValue == value
}
}
if showDiscard {
HStack {
Spacer()
buttonsStack
.padding(4)
}
.animation(.linear,value: showDiscard)
.transition(.move(edge: !showDiscard ? .bottom : .top).combined(with: .opacity))
}
}
}
private var buttonsStack: some View {
GlassEffectContainer {
HStack(spacing: 12.0) {
Button {
withAnimation {
pendingValue = value
}
} label: {
Label(”Cancel”, systemImage: “xmark.circle”)
.labelStyle(.iconOnly)
.foregroundStyle(.red)
}
.frame(width: 40, height: 40)
.buttonStyle(.glass)
Button {
value = pendingValue
} label: {
Label(”Save”, systemImage: “checkmark.circle.fill”)
.labelStyle(.iconOnly)
.font(.title3)
.foregroundStyle(.green)
}
.frame(width: 40, height: 40)
.buttonStyle(.glass)
}
}
}
}Right now we have:
withAnimationwraps changes for animation variablePanel visibility linked to that variable
transitionwith combined animations depending on appearance logic
However, you must have been seen a small glitch. Panel might not discard all the time on slider change. All lies in the value comparison:
showDiscard = pendingValue == valueYou can’t compare float-point values like that. Counting accuracy between values is not just more precise - it’s the correct way:
showDiscard = abs(pendingValue - value) > 0.1Conclusion
We’ve created a convenient wrapper around Slider to show actions panel and trigger initial value change only if needed. This approach will prevent redundant actions and storage overhead.
One more thing…
Ever tried to explain “Yak Shaving,” “Spaghetti Code,” or “Imposter Syndrome”? Now you don’t have to — just send a sticker.
TecTalk turns everyday developer slang into fun, relatable stickers for your chats. Whether you’re venting about bugs or celebrating a successful deploy, there’s a sticker for every tech mood.
Created by me 🧑💻 — made by a dev, for devs — and available now at a very affordable price.
Express your inner techie. Stop typing. Start sticking.





