How to Drag a Working Slider Using Swiftui

How to drag a working slider using SwiftUI

This is working. Basically I needed to set the flag in an outside observable object for it to update the view so that it could take effect. When the value changes the flag is set to false, but then after a tenth of a second it becomes draggable. Working pretty seamlessly.

struct ContentView: View {
@State var pos = CGSize.zero
@State var acc = CGSize.zero
@State var value = 0.0

@ObservedObject var model = Model()

var body: some View {
let drag = DragGesture()
.onChanged { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
}
.onEnded { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
self.acc = self.pos
}

return VStack {
Slider(value: $value, in: 0...100, step: 1) { _ in
self.model.flag = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.model.flag = true
}
}
}
.frame(width: 250, height: 40, alignment: .center)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
.offset(x: self.pos.width, y: self.pos.height)
.gesture(model.flag == true ? drag : nil)
}
}

class Model: ObservableObject {
@Published var flag = false
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

DragGesture blocks touching a Slider in SwiftUI

See if this solution works for you. As mentioned in my comment it's basically overwriting the gesture reader of the slider completely. To get the width I used a geometry reader. You could probably even use .gesture or even .highPriorityGesture instead of simultaneousGesture Also depending where you place the GeometryReader you might have to use the .local coordinateSpace of the gesture.

struct ContentView: View {

@State private var progress: TimeInterval = 0
@State private var sliderMoving: Bool = false

var body: some View {

GeometryReader { geometry in

Slider(value: $progress, in: 0 ... Double(100), onEditingChanged: { didChange in

}).simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
print("gesture onChanged")
sliderMoving = true
progress = TimeInterval(gesture.location.x / (geometry.size.width / 100))
}
.onEnded { gesture in
print("gesture onEnded")
sliderMoving = false
progress = TimeInterval(gesture.location.x / (geometry.size.width / 100))
}
)

}
}
}

Update with addition to comment below. This optionally adjusts to the padding modifier and knob size. Depending how the slider is setup different adjustments might be needed to get the exact position. I'm not currently aware of a way to get the exact locations of individual parts of the slider. A custom slider might solve this problem.

struct ContentView6: View {

@State private var progress: TimeInterval = 0
@State private var sliderMoving: Bool = false

var body: some View {

GeometryReader { geometry in

let padding: CGFloat = 0 //optional in case padding needs to be adjusted.
let adjustment: CGFloat = padding + 15

Slider(value: $progress, in: 0 ... Double(100), onEditingChanged: { didChange in

})
.padding(padding)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
sliderMoving = true
progress = TimeInterval( min(max((gesture.location.x - adjustment) / ((geometry.size.width - adjustment*2) / 100), 0), 100) )
print(progress)
}
.onEnded { gesture in
sliderMoving = false
}
)

}
}
}

SwiftUI: How to move Slider Value with Thumb

The problem here is that we do not have access to nob size and positioning (which are private) and refer only to range and current value, and this leads to some misalignment between slider's nob and hover value.

Anyway here is possible approach using alignment guide. Tuning to nob size I leave for you.

Tested with Xcode 13.4 / iOS 15.5

demo

struct SliderView: View {
@State private var speed = 50.0

var minValue: Double = 1
var maxValue: Double = 100

var body: some View {
HStack {
Text("\(minValue,specifier: "%.f")")
Slider(value: $speed, in: minValue...maxValue, step: 1)
.alignmentGuide(VerticalAlignment.center) { $0[VerticalAlignment.center]}
.padding(.top)
.overlay(GeometryReader { gp in
Text("\(speed,specifier: "%.f")").foregroundColor(.blue)
.alignmentGuide(HorizontalAlignment.leading) {
$0[HorizontalAlignment.leading] - (gp.size.width - $0.width) * speed / ( maxValue - minValue)
}
.frame(maxWidth: .infinity, alignment: .leading)
}, alignment: .top)
Text("\(maxValue,specifier: "%.f")")
}
.padding()
}
}

Test module on GitHub

How can I call a function when the user drop the slider with swiftUI?

Try the onEditingChanged closure parameter. It receives a Bool set to true when the change begins and false when the change ends.

Slider(value: $value, in: 0...200, step: 10, onEditingChanged: { bool in
print("\(bool)")
})

How to trigger a function when a Slider changes in SwiftUI?

You can make a custom Binding that calls the function in the Binding's setter:

@State var progress: Float = 0.5
var body: some View {

VStack{
Slider(value: Binding(get: {
self.progress
}, set: { (newVal) in
self.progress = newVal
self.sliderChanged()
}))
.padding(.all)
Text(String(progress))
}
}

func sliderChanged() {
print("Slider value changed to \(progress)")
}

SwiftUI - 2 Handle Range Slider

I've created a custom slider for you. I hope that's enough for your needs. Let me know if there is anything else I can do.

Sample Image

Slider:

import SwiftUI
import Combine

//SliderValue to restrict double range: 0.0 to 1.0
@propertyWrapper
struct SliderValue {
var value: Double

init(wrappedValue: Double) {
self.value = wrappedValue
}

var wrappedValue: Double {
get { value }
set { value = min(max(0.0, newValue), 1.0) }
}
}

class SliderHandle: ObservableObject {

//Slider Size
let sliderWidth: CGFloat
let sliderHeight: CGFloat

//Slider Range
let sliderValueStart: Double
let sliderValueRange: Double

//Slider Handle
var diameter: CGFloat = 40
var startLocation: CGPoint

//Current Value
@Published var currentPercentage: SliderValue

//Slider Button Location
@Published var onDrag: Bool
@Published var currentLocation: CGPoint

init(sliderWidth: CGFloat, sliderHeight: CGFloat, sliderValueStart: Double, sliderValueEnd: Double, startPercentage: SliderValue) {
self.sliderWidth = sliderWidth
self.sliderHeight = sliderHeight

self.sliderValueStart = sliderValueStart
self.sliderValueRange = sliderValueEnd - sliderValueStart

let startLocation = CGPoint(x: (CGFloat(startPercentage.wrappedValue)/1.0)*sliderWidth, y: sliderHeight/2)

self.startLocation = startLocation
self.currentLocation = startLocation
self.currentPercentage = startPercentage

self.onDrag = false
}

lazy var sliderDragGesture: _EndedGesture<_ChangedGesture<DragGesture>> = DragGesture()
.onChanged { value in
self.onDrag = true

let dragLocation = value.location

//Restrict possible drag area
self.restrictSliderBtnLocation(dragLocation)

//Get current value
self.currentPercentage.wrappedValue = Double(self.currentLocation.x / self.sliderWidth)

}.onEnded { _ in
self.onDrag = false
}

private func restrictSliderBtnLocation(_ dragLocation: CGPoint) {
//On Slider Width
if dragLocation.x > CGPoint.zero.x && dragLocation.x < sliderWidth {
calcSliderBtnLocation(dragLocation)
}
}

private func calcSliderBtnLocation(_ dragLocation: CGPoint) {
if dragLocation.y != sliderHeight/2 {
currentLocation = CGPoint(x: dragLocation.x, y: sliderHeight/2)
} else {
currentLocation = dragLocation
}
}

//Current Value
var currentValue: Double {
return sliderValueStart + currentPercentage.wrappedValue * sliderValueRange
}
}

class CustomSlider: ObservableObject {

//Slider Size
let width: CGFloat = 300
let lineWidth: CGFloat = 8

//Slider value range from valueStart to valueEnd
let valueStart: Double
let valueEnd: Double

//Slider Handle
@Published var highHandle: SliderHandle
@Published var lowHandle: SliderHandle

//Handle start percentage (also for starting point)
@SliderValue var highHandleStartPercentage = 1.0
@SliderValue var lowHandleStartPercentage = 0.0

var anyCancellableHigh: AnyCancellable?
var anyCancellableLow: AnyCancellable?

init(start: Double, end: Double) {
valueStart = start
valueEnd = end

highHandle = SliderHandle(sliderWidth: width,
sliderHeight: lineWidth,
sliderValueStart: valueStart,
sliderValueEnd: valueEnd,
startPercentage: _highHandleStartPercentage
)

lowHandle = SliderHandle(sliderWidth: width,
sliderHeight: lineWidth,
sliderValueStart: valueStart,
sliderValueEnd: valueEnd,
startPercentage: _lowHandleStartPercentage
)

anyCancellableHigh = highHandle.objectWillChange.sink { _ in
self.objectWillChange.send()
}
anyCancellableLow = lowHandle.objectWillChange.sink { _ in
self.objectWillChange.send()
}
}

//Percentages between high and low handle
var percentagesBetween: String {
return String(format: "%.2f", highHandle.currentPercentage.wrappedValue - lowHandle.currentPercentage.wrappedValue)
}

//Value between high and low handle
var valueBetween: String {
return String(format: "%.2f", highHandle.currentValue - lowHandle.currentValue)
}
}

Slider implementation:

import SwiftUI

struct ContentView: View {
@ObservedObject var slider = CustomSlider(start: 10, end: 100)

var body: some View {
VStack {
Text("Value: " + slider.valueBetween)
Text("Percentages: " + slider.percentagesBetween)

Text("High Value: \(slider.highHandle.currentValue)")
Text("Low Value: \(slider.lowHandle.currentValue)")

//Slider
SliderView(slider: slider)
}
}
}

struct SliderView: View {
@ObservedObject var slider: CustomSlider

var body: some View {
RoundedRectangle(cornerRadius: slider.lineWidth)
.fill(Color.gray.opacity(0.2))
.frame(width: slider.width, height: slider.lineWidth)
.overlay(
ZStack {
//Path between both handles
SliderPathBetweenView(slider: slider)

//Low Handle
SliderHandleView(handle: slider.lowHandle)
.highPriorityGesture(slider.lowHandle.sliderDragGesture)

//High Handle
SliderHandleView(handle: slider.highHandle)
.highPriorityGesture(slider.highHandle.sliderDragGesture)
}
)
}
}

struct SliderHandleView: View {
@ObservedObject var handle: SliderHandle

var body: some View {
Circle()
.frame(width: handle.diameter, height: handle.diameter)
.foregroundColor(.white)
.shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 0)
.scaleEffect(handle.onDrag ? 1.3 : 1)
.contentShape(Rectangle())
.position(x: handle.currentLocation.x, y: handle.currentLocation.y)
}
}

struct SliderPathBetweenView: View {
@ObservedObject var slider: CustomSlider

var body: some View {
Path { path in
path.move(to: slider.lowHandle.currentLocation)
path.addLine(to: slider.highHandle.currentLocation)
}
.stroke(Color.green, lineWidth: slider.lineWidth)
}
}

How does one change the position of a Slider in SwiftUI?

example:

struct ContentView: View {

@State var number : Float

var body: some View {
VStack {
Slider(value: $number, in: 1...100)
.padding()
Text("You've selected \(Int(number))")
}.onAppear() {
self.number = 30
}
}
}

SwiftUI: How to get continuous updates from Slider

In SwiftUI, you can bind UI elements such as slider to properties in your data model and implement your business logic there.

For example, to get continuous slider updates:

import SwiftUI
import Combine

final class SliderData: BindableObject {

let didChange = PassthroughSubject<SliderData,Never>()

var sliderValue: Float = 0 {
willSet {
print(newValue)
didChange.send(self)
}
}
}

struct ContentView : View {

@EnvironmentObject var sliderData: SliderData

var body: some View {
Slider(value: $sliderData.sliderValue)
}
}

Note that to have your scene use the data model object, you need to update your window.rootViewController to something like below inside SceneDelegate class, otherwise the app crashes.

window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(SliderData()))

Sample Image



Related Topics



Leave a reply



Submit