Using a UIViewRepresentable like this works, but the Liquid Glass animations for the slider look very wonky and choppy. On slider release there isn't even an animation at all.
import SwiftUI
import UIKit
struct ContentView: View {
@State private var sliderValue: Double = 0.48
var body: some View {
VStack {
VStack {
SafeSlider(value: $sliderValue, range: 0...1.18, step: 0.01)
Text("Value: \(sliderValue, specifier: "%.2f")")
}
.frame(maxWidth: 128)
}
.padding()
}
}
struct SafeSlider<Value: BinaryFloatingPoint>: View where Value.Stride: BinaryFloatingPoint {
@Binding var value: Value
var range: ClosedRange<Value> = 0...1
var step: Value = 0
var onEditingChanged: (Bool) -> Void = { _ in }
var body: some View {
#if targetEnvironment(macCatalyst)
if #available(macOS 26.0, *) {
SliderWrapper(value: $value, range: range, step: step, onEditingChanged: onEditingChanged)
} else {
defaultSlider
}
#else
defaultSlider
#endif
}
private var defaultSlider: some View {
Slider(value: $value, in: range, step: Value.Stride(step), onEditingChanged: onEditingChanged)
}
}
// MARK: - UIKit wrapper for Catalyst
#if targetEnvironment(macCatalyst)
struct SliderWrapper<Value: BinaryFloatingPoint>: UIViewRepresentable where Value.Stride: BinaryFloatingPoint {
@Binding var value: Value
var range: ClosedRange<Value>
var step: Value
var onEditingChanged: (Bool) -> Void
func makeUIView(context: Context) -> UISlider {
let slider = UISlider()
slider.minimumValue = Float(range.lowerBound)
slider.maximumValue = Float(range.upperBound)
slider.value = Float(value)
slider.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged(_:)), for: .valueChanged)
slider.addTarget(context.coordinator, action: #selector(Coordinator.dragStarted(_:)), for: .touchDown)
slider.addTarget(context.coordinator, action: #selector(Coordinator.dragEnded(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
return slider
}
func updateUIView(_ uiView: UISlider, context: Context) {
uiView.minimumValue = Float(range.lowerBound)
uiView.maximumValue = Float(range.upperBound)
uiView.value = Float(value)
}
func makeCoordinator() -> Coordinator {
Coordinator(value: $value, step: step, onEditingChanged: onEditingChanged)
}
class Coordinator: NSObject {
var value: Binding<Value>
var step: Value
var onEditingChanged: (Bool) -> Void
init(value: Binding<Value>, step: Value, onEditingChanged: @escaping (Bool) -> Void) {
self.value = value
self.step = step
self.onEditingChanged = onEditingChanged
}
@objc func valueChanged(_ sender: UISlider) {
var newValue = Value(sender.value)
if step != 0 {
let rounded = (newValue / step).rounded() * step
newValue = rounded
sender.value = Float(newValue)
}
value.wrappedValue = newValue
}
@objc func dragStarted(_ sender: UISlider) {
onEditingChanged(true)
}
@objc func dragEnded(_ sender: UISlider) {
onEditingChanged(false)
}
}
}
#endif