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
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.
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()))
Related Topics
Realm + Nstableview + Nsarraycontroller
Write Data to Firebase in The Background After Retrieving Steps with Healthkit's Background Delivery
Adding Data to a Specific UId in Firebase
Macos Remote Push Notifications Not Showing Alert Banner
How to Constrain Second Nsviewcontroller Minimum Size in Os X App
Can't Hide Status Bar in Avplayerviewcontroller's Portrait Mode
Cannot Use Mutating Member on Immutable Value of Type 'string'
Prefix(_ Maxlength:) Is Type-Erased When Used with a Struct That Conforms to Lazysequenceprotocol
Add Assets to an Icloud Shared Photo Album Programmatically
Arkit: Place Object on a Plane Doesn't Work Properly
How to Drag a Working Slider Using Swiftui
Indent Second Line of UIlabel (Swift)
Msmessagelivelayout Freeze/Crash in Transcript When Info.Plist Contains Privacy Request
Image to String Using Base64 in Swift 4
Audiokit Seems to Receive Only The First Three Numbers of Sysex Midi Messages