cannot add tappable item into slider
Hi.
I'd like to be able to add tappable items into the slider.
something like:
HStack { Text("Left").onTapGesture(...); Spacer(); Text("Right").onTapGesture(...) }
I have tried using the .compactSliderBackground and .overlay and .background
If I use the .overlay I can get it to work, but if I try to drag the slider starting on one the tappable elements it won't drag.
I would think the proper place to add this is the .compactSliderBackground but I think maybe how the slider gestures are registered do not allow this to work.
I am using 2.0.8 on OSX 15.3
Any ideas? Thanks.
I tried playing with the minimum drag distance option, and that didn't help.
I also tried using the simultaneous and high priority options - couldn't make it work.
Is there an example of something like this I can review?
Hi! Could you try to use simultaneous for the tap gestures?
Hi. I have. Should I add them to the compactSliderBackground? Or to an overlay?
You could add it to the overlay with your labels.
Here is the code. I cannot get the labels to be clickable and also be able to start a drag of the slider not matter what I try. Thank you for your help.
import SwiftUI
import CompactSlider
struct MySlider : View {
@State var value : Double = 0.5
var body: some View {
CompactSlider(value: $value, in: 0...1, step: 0.05)
.compactSliderOptionsByAdding(.snapToSteps,.highPriorityGesture,.scrollWheel,.dragGestureMinimumDistance(5))
.compactSliderScale(
visibility: .default,
alignment: .top,
.linear(count: 21, lineLength: 8, skipFirst: 1, skipLast: 1, startFromCenter: true)
)
.overlay {
HStack {
Text("Click #1")
.lineLimit(1)
.minimumScaleFactor(0.5)
.clipShape(Rectangle())
.onTapGesture {
print("Click #1 was clicked!")
}
Spacer()
Text("Click #2")
.lineLimit(1)
.minimumScaleFactor(0.5)
.clipShape(Rectangle())
.simultaneousGesture (
TapGesture()
.onEnded {
print("Click #2 was clicked!")
}
)
}
}
.frame(width:200, height:24)
.onAppear {
print("I'm here!")
}
}
}
#Preview {
MySlider()
}
Do you have any ideas for this? It should be possible (I think), as a SwiftUI you can add a tap gesture to an item, but even if you do, the drag and drop of the item to reorder in the list still works.
I would have thought that increasing the drag gesture distance requirement on the slider would have fixed it but it did not.
I was thinking to add an event inside the slider to detect if there was a tap and provide a location of it. Will it help?
Thanks for looking into it. I decided to just roll my own simple slider and it works as expected so I’m not sure what the interaction is that causes CompactSlider not to work.
I've decided to add an option to provide an external drag gestures from overlay layers.
One note, I’ve found supporting multiple drag gestures problematic. You almost always need to simulate a tap using a drag with minimum distance of 0. It gets messy and error prone.
To he honest I was surprised my slider solution worked based on past experiences, but it seems to distinguish between the tap on the labels and the drag on the background/scale/handle perfectly.
TLDR I would try and support taps and implement the component behaviors with drags.
That's great! Please share your solution, maybe I could improve mine.
Currently I'm testing this:
CompactSlider(value: $value, in: 0...1)
.compactSliderExternalDragGesture(externalDragGesture)
.overlay {
HStack {
Text("Tap on Text #1")
.lineLimit(1)
.minimumScaleFactor(0.5)
.clipShape(Rectangle())
.onTapGesture {
tapEffect = "Tap #1 on text!"
}
.simultaneousGesture(
DragGesture()
.onChanged { externalDragGesture = .init(onChanged: $0) }
.onEnded { externalDragGesture = .init(onEnded: $0) }
)
// ...
Ah I see. That is different than what I was thinking and how my solution works. Give me a bit to simplify the code to the essentials and I’ll share. It makes a lot of assumptions but it works in my case.
I've been able to make it work, but trying to simplify the API.
Here is my code. It's not the real version but enough to demonstrate the dragging starting on the labels, and the ability to tap the labels as well.
//
// MySlider.swift
//
// Created by robert engels on 12/2/24.
//
import Foundation
import SwiftUI
import Swift
private struct SliderScale : Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let minorSpacing = rect.width / 20
let majorSpacing = rect.width / 10
for x in stride(from: 0, to: rect.width, by: minorSpacing) {
path.move(to: CGPoint(x:x,y:0))
path.addLine(to: CGPoint(x:x,y:rect.height * 0.20))
}
for x in stride(from: 0, to: rect.width, by: majorSpacing) {
path.move(to: CGPoint(x:x,y:0))
path.addLine(to: CGPoint(x:x,y:rect.height * 0.40))
}
return path.offsetBy(dx: rect.origin.x, dy: rect.origin.y)
}
}
private func range_percent<T: BinaryFloatingPoint>(at value: T, in range: ClosedRange<T>) -> Double {
return Double((value - range.lowerBound) / (range.upperBound - range.lowerBound))
}
private struct HandleView : Shape {
@State var percent : Double
func path(in rect: CGRect) -> Path {
var path = Path()
let x = rect.width * Double(percent)
path.move(to: CGPoint(x:x,y:0))
path.addLine(to: CGPoint(x:x,y:rect.height))
return path.offsetBy(dx: rect.origin.x, dy: rect.origin.y)
}
}
struct MySlider<T : BinaryFloatingPoint & Comparable & Sendable> : View {
private var label : String
private var value : Binding<T>
private var bounds: ClosedRange<T>
private var step : T
@State private var active = false
@State private var dragStart : T?
init(label: String, value: Binding<T>, in bounds: ClosedRange<T> = 0...1, step: T) {
self.label = label
self.value = value
self.bounds = bounds
self.step = step
}
var dragGesture: some Gesture {
DragGesture(minimumDistance: 1.0)
.onChanged { v in
if dragStart == nil {
dragStart = value.wrappedValue
}
// Total logical range
let totalRange = CGFloat(bounds.upperBound - bounds.lowerBound)
// Define how many pixels equal the full range (adjust as needed)
let dragWidth: CGFloat = 200 // e.g., 200 points = full range
let scale = totalRange / dragWidth
let delta = v.translation.width * scale
let newValue = CGFloat(dragStart!) + delta
value.wrappedValue = min(max(T(newValue), bounds.lowerBound), bounds.upperBound)
}
.onEnded { _ in
dragStart = nil
}
}
var body: some View {
ZStack
{
RoundedRectangle(cornerRadius: 6)
.fill(.gray.opacity(0.125))
SliderScale()
.stroke(.gray.opacity(0.50), lineWidth: 2)
.opacity(active ? 1.0 : 0.0)
HandleView(percent: range_percent(at: value.wrappedValue, in: bounds))
.stroke(active ? AnyShapeStyle(.tint) : AnyShapeStyle(Color.gray.opacity(0.75)), lineWidth: 3)
HStack {
Text(label)
.lineLimit(1)
.minimumScaleFactor(0.5)
.clipShape(Rectangle())
.highPriorityGesture(TapGesture().onEnded {
value.wrappedValue = max(value.wrappedValue - step,bounds.lowerBound)
})
Spacer()
Text(String(format: "%0.2f", Double(value.wrappedValue)))
.lineLimit(1)
.minimumScaleFactor(0.25)
.clipShape(Rectangle())
.onTapGesture {
value.wrappedValue = max(value.wrappedValue + step,bounds.lowerBound)
}
}
.padding(3)
}
.highPriorityGesture(dragGesture)
.frame(height:24, alignment: .top)
.onHover { hovering in self.active = hovering }
.frame(height:24)
}
}
#Preview {
@Previewable @State var value : Double = 0.5
MySlider(label: "My Label", value: $value, in: 0...1, step: 0.05)
}