What I want to get is a UISlider
which lets the user not only slide when he starts on its thumbRect
, but also when he taps elsewhere. When the user taps on the slider but outside of the thumbRect
, the slider should jump to that value and then still keeping up to the user's sliding gesture.
What I have tried so far was implementing a subclass of UIGestureRecognizer like in this suggestion. It starts right then when a touch down somewhere outside the thumbRect
occurs. The problem is that the slider sets its value but then further sliding gestures are ignored because the touch down recognizer has stolen the touch.
How can I implement a slider where you can tap anywhere but still slide right away?
Edit: ali59a was so kind to add an example of what I've done now. This requires to lift the finger again, after that I can touch and drag to slide (a tap is not what I want, I need a 'touch and slide' right away).
I converted the answer provided by DWilliames to Swift
Inside your viewDidAppear()
let longPress = UILongPressGestureRecognizer(target: self.slider, action: Selector("tapAndSlide:"))
longPress.minimumPressDuration = 0
self.addGestureRecognizer(longPress)
Class file
class TapUISlider: UISlider
{
func tapAndSlide(gesture: UILongPressGestureRecognizer)
{
let pt = gesture.locationInView(self)
let thumbWidth = self.thumbRect().size.width
var value: Float = 0
if (pt.x <= self.thumbRect().size.width / 2)
{
value = self.minimumValue
}
else if (pt.x >= self.bounds.size.width - thumbWidth / 2)
{
value = self.maximumValue
}
else
{
let percentage = Float((pt.x - thumbWidth / 2) / (self.bounds.size.width - thumbWidth))
let delta = percentage * (self.maximumValue - self.minimumValue)
value = self.minimumValue + delta
}
if (gesture.state == UIGestureRecognizerState.Began)
{
UIView.animateWithDuration(0.35, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut,
animations:
{
self.setValue(value, animated: true)
super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
},
completion: nil)
}
else
{
self.setValue(value, animated: false)
}
}
func thumbRect() -> CGRect
{
return self.thumbRectForBounds(self.bounds, trackRect: self.bounds, value: self.value)
}
}
I'm not sure if you are still looking for an answer for this, but I was just looking at this myself today; and I managed to get it to work for me.
The key to it, is using a UILongPressGestureRecognizer
instead of just a UITapGestureRecognizer
, we can then set the minimumPressDuration
of the recognizer to 0; making it act as a tap recognizer, except you can now actually check its state.
Putting what ali59a suggested will work for you, just by replacing the UITapGestureRecognizer
with a UILongPressGestureRecognizer
. However, I found that this didn't seem to quite put the thumbRect directly under my thumb. It appeared a bit off to me.
I created my own UISlider subclass for my project, and here is how I implemented the "tap and slide feature" for me.
In my init
method:
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(tapAndSlide:)];
longPress.minimumPressDuration = 0;
[self addGestureRecognizer:longPress];
Then my tapAndSlide:
method:
- (void)tapAndSlide:(UILongPressGestureRecognizer*)gesture
{
CGPoint pt = [gesture locationInView: self];
CGFloat thumbWidth = [self thumbRect].size.width;
CGFloat value;
if(pt.x <= [self thumbRect].size.width/2.0)
value = self.minimumValue;
else if(pt.x >= self.bounds.size.width - thumbWidth/2.0)
value = self.maximumValue;
else {
CGFloat percentage = (pt.x - thumbWidth/2.0)/(self.bounds.size.width - thumbWidth);
CGFloat delta = percentage * (self.maximumValue - self.minimumValue);
value = self.minimumValue + delta;
}
if(gesture.state == UIGestureRecognizerStateBegan){
[UIView animateWithDuration:0.35 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
[self setValue:value animated:YES];
[super sendActionsForControlEvents:UIControlEventValueChanged];
} completion:nil];
}
else [self setValue:value];
if(gesture.state == UIGestureRecognizerStateChanged)
[super sendActionsForControlEvents:UIControlEventValueChanged];
}
Where I also used a method to return the frame of my custom thumbRect:
- (CGRect)thumbRect {
CGRect trackRect = [self trackRectForBounds:self.bounds];
return [self thumbRectForBounds:self.bounds trackRect:trackRect value:self.value];
}
I also have my slider animate to the position where the user first taps, over 0.35 seconds. Which I reckon looks pretty sweet, so I included that in that code. If you don't want that, simply try this:
- (void)tapAndSlide:(UILongPressGestureRecognizer*)gesture
{
CGPoint pt = [gesture locationInView: self];
CGFloat thumbWidth = [self thumbRect].size.width;
CGFloat value;
if(pt.x <= [self thumbRect].size.width/2.0)
value = self.minimumValue;
else if(pt.x >= self.bounds.size.width - thumbWidth/2.0)
value = self.maximumValue;
else {
CGFloat percentage = (pt.x - thumbWidth/2.0)/(self.bounds.size.width - thumbWidth);
CGFloat delta = percentage * (self.maximumValue - self.minimumValue);
value = self.minimumValue + delta;
}
[self setValue:value];
if(gesture.state == UIGestureRecognizerStateChanged)
[super sendActionsForControlEvents:UIControlEventValueChanged];
}
I hope that makes sense, and helps you.
You should add a tap gesture on your UISlider
.
Exemple :
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(sliderTapped:)];
[_slider addGestureRecognizer:tapGestureRecognizer];
In sliderTapped you can get the location and update the value of the slider :
- (void)sliderTapped:(UIGestureRecognizer *)gestureRecognizer {
CGPoint pointTaped = [gestureRecognizer locationInView:gestureRecognizer.view];
CGPoint positionOfSlider = _slider.frame.origin;
float widthOfSlider = _slider.frame.size.width;
float newValue = ((pointTaped.x - positionOfSlider.x) * _slider.maximumValue) / widthOfSlider;
[_slider setValue:newValue];
}
I create an example here : https://github.com/ali59a/tap-and-slide-in-a-UISlider
Here's my modification to the above:
class TapUISlider: UISlider {
func tapAndSlide(gesture: UILongPressGestureRecognizer) {
let pt = gesture.locationInView(self)
let thumbWidth = self.thumbRect().size.width
var value: Float = 0
if (pt.x <= self.thumbRect().size.width / 2) {
value = self.minimumValue
} else if (pt.x >= self.bounds.size.width - thumbWidth / 2) {
value = self.maximumValue
} else {
let percentage = Float((pt.x - thumbWidth / 2) / (self.bounds.size.width - thumbWidth))
let delta = percentage * (self.maximumValue - self.minimumValue)
value = self.minimumValue + delta
}
if (gesture.state == UIGestureRecognizerState.Began) {
UIView.animateWithDuration(0.35, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut,
animations: {
self.setValue(value, animated: true)
super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
}, completion: nil)
} else {
self.setValue(value, animated: false)
super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
}
}
func thumbRect() -> CGRect {
return self.thumbRectForBounds(self.bounds, trackRect: self.bounds, value: self.value)
}
}
Adding swift version of Ali AB.'s answer,
@IBAction func sliderTappedAction(sender: UITapGestureRecognizer)
{
if let slider = sender.view as? UISlider {
if slider.highlighted { return }
let point = sender.locationInView(slider)
let percentage = Float(point.x / CGRectGetWidth(slider.bounds))
let delta = percentage * (slider.maximumValue - slider.minimumValue)
let value = slider.minimumValue + delta
slider.setValue(value, animated: true)
}
}
I didn't check David Williames answer, but I'll post my solution in case someone is looking for another way to do it.
Swift 4
First create a custom UISlider so that it will detect touches on the bar as well :
class CustomSlider: UISlider {
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
return true
}
}
(don't forget to set your slider to be this CustomSlider, on storyboard)
The on viewDidLoad of the view controller that is displaying the slider:
self.slider.addTarget(self, action: #selector(sliderTap), for: .touchDown)
(this is only used to pause the player when moving the slider)
Then, on your UISlider action:
@IBAction func moveSlider(_ sender: CustomSlider, forEvent event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .ended, .cancelled, .stationary:
//here, start playing if needed
startPlaying()
default:
break
}
}
}
And on your "sliderTap" selector method:
@objc func sliderTap() {
//pause the player, if you want
audioPlayer?.pause()
}
Suggestion: set the player "currentTime" before starting to play:
private func startPlaying() {
audioPlayer?.currentTime = Double(slider.value)
audioPlayer?.play()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With