How to enable "tap and slide" in a UISlider?
Asked Answered
K

14

20

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).

Kreda answered 28/3, 2014 at 15:36 Comment(2)
This is how sliders worked in iOS 2.0. I don't know why Apple changed it.Vaas
possible duplicate of iPhone:Programming UISlider to position at clicked locationBoswell
C
24

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.

Corena answered 10/4, 2014 at 7:59 Comment(0)
E
13

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)
    }
}
Erysipeloid answered 25/8, 2015 at 7:41 Comment(2)
One minor modification to the above worked for me. Both spots where you call self.setValue, I also needed to fire off super.sendActionsForControlEvents(UIControlEvents.ValueChanged)Klemens
And how do I update my music player? the idea is that as it makes the new displacement, I get the values to modify the time in my music player, what approach could be given?Chlamydospore
M
4

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

Macroscopic answered 28/3, 2014 at 16:28 Comment(2)
Thanks for editing the title of my question, but it's not matching the question: As I wrote, I want to be able to touch anywhere and then be able to scroll without(!) lifting the finger (not a tap)! This is what I already did, but unfortunately I have to lift the finger again before I can use slide to change the value.Kreda
Seems not to work. UISlider doesn't see any recognizer, but if I add it to the superview that has UIView class - everything is ok. Any ideas?Diligent
K
3

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)
  }
}
Klemens answered 10/10, 2015 at 18:52 Comment(0)
S
3

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)
    }
}
Sideways answered 14/1, 2016 at 12:26 Comment(0)
C
2

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()
}
Chromyl answered 16/11, 2017 at 2:29 Comment(0)
E
2

Updated tsji10dra's answer to Swift 4:

@IBAction func sliderTappedAction(sender: UITapGestureRecognizer) {

    if let slider = sender.view as? UISlider {
        if slider.isHighlighted { return }

        let point = sender.location(in: slider)
        let percentage = Float(point.x / slider.bounds.size.width)
        let delta = percentage * (slider.maximumValue - slider.minimumValue)
        let value = slider.minimumValue + delta
        slider.setValue(value, animated: true)

        // also remember to call valueChanged if there's any
        // custom behaviour going on there and pass the slider
        // variable as the parameter, as indicated below
        self.sliderValueChanged(slider)
    }
}
Engrain answered 29/1, 2018 at 15:47 Comment(0)
K
2

My solution is quite simple:

class CustomSlider: UISlider {
    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        let newValue = <calculated_value>
        self.setValue(newValue, animated: false)
        super.sendActions(for: UIControlEvents.valueChanged)
        return true
}}
Kilgore answered 6/2, 2018 at 18:2 Comment(0)
A
2

This works for me in iOS 13.6 & 14.0
No need to add gesture only override beginTracking function like this :

@objc
private func sliderTapped(touch: UITouch) {
    let point = touch.location(in: self)
    let percentage = Float(point.x / self.bounds.width)
    let delta = percentage * (self.maximumValue - self.minimumValue)
    let newValue = self.minimumValue + delta
    if newValue != self.value {
        value = newValue
        sendActions(for: .valueChanged)
    }
}

override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    sliderTapped(touch: touch)
    return true
}
Agile answered 9/11, 2020 at 21:49 Comment(0)
C
1

I completed @DWilliames solution for a UISlider subclass containing minimum and maximumValueImages.

Additionally I implemented a functionality for user touches in the areas outside the trackArea (means either the area around the minimum or the maximumValueImage). Touching these areas moves the slider/changes the value in intervals.

- (void) tapAndSlide: (UILongPressGestureRecognizer*) gesture {
    CGPoint touchPoint = [gesture locationInView: self];
    CGRect trackRect = [self trackRectForBounds: self.bounds];
    CGFloat thumbWidth = [self thumbRectForBounds: self.bounds trackRect: trackRect value: self.value].size.width;
    CGRect trackArea = CGRectMake(trackRect.origin.x, 0, trackRect.size.width, self.bounds.size.height);
    CGFloat value;

if (CGRectContainsPoint(trackArea, touchPoint)) {
    if (touchPoint.x <= trackArea.origin.x + thumbWidth/2.0) {
        value = self.minimumValue;
    }
    else if (touchPoint.x >= trackArea.origin.x + trackArea.size.width - thumbWidth/2.0) {
        value = self.maximumValue;
    }
    else {
        CGFloat percentage = (touchPoint.x - trackArea.origin.x - thumbWidth/2.0)/(trackArea.size.width - thumbWidth);
        CGFloat delta = percentage*(self.maximumValue - self.minimumValue);
        value = self.minimumValue + delta;
    }

    if (value != self.value) {
        if (gesture.state == UIGestureRecognizerStateBegan) {
            [UIView animateWithDuration: 0.2 delay: 0 options: UIViewAnimationOptionCurveEaseInOut animations: ^{
                [self setValue: value animated: YES];

            } completion: ^(BOOL finished) {
                [self sendActionsForControlEvents: UIControlEventValueChanged];
            }];
        }
        else {
            [self setValue: value animated: YES];
            [self sendActionsForControlEvents: UIControlEventValueChanged];
        }
    }
}
else {
    if (gesture.state == UIGestureRecognizerStateBegan) {
        if (touchPoint.x <= trackArea.origin.x) {
            if (self.value == self.minimumValue) return;
            value = self.value - 1.5;
        }
        else {
            if (self.value == self.maximumValue) return;
            value = self.value + 1.5;
        }
        CGFloat duration = 0.1;
        [UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveEaseInOut animations: ^{
            [self setValue: value animated: YES];
        } completion: ^(BOOL finished) {
            [self sendActionsForControlEvents: UIControlEventValueChanged];
        }];
    }
}
}
Cussed answered 12/10, 2015 at 17:0 Comment(0)
R
1

To expand on the answer of Khang Azun- for swift 5 put the following in a UISlider custom class:

override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    let percent = Float(touch.location(in: self).x / bounds.size.width)
    let delta = percent * (maximumValue - minimumValue)

    let newValue = minimumValue + delta
    self.setValue(newValue, animated: false)
    super.sendActions(for: UIControl.Event.valueChanged)
    return true
}
Rugose answered 14/6, 2019 at 7:37 Comment(1)
This doesn't take into consideration any min or max value images that may be present, which means that if you do have min/max images, tapping the beginning or ending of the slider bar won't skip all the way to the beginning or end of the media. You would think that UISlider.trackRect(forBounds:) could be used to get the bounds of the slider without its min/max images, but Apple's documentation explicitly states that this shouldn't be called directly. I suppose that you can get the min/max images and calculate the slider bounds explicitly.Betony
M
0

At the risk of being chastised by the iOS pure community...

Here is a solution for Xamarin iOS C# converted from David Williames Answer.

Sub class UISlider:

[Register(nameof(UISliderCustom))]
[DesignTimeVisible(true)]
public class UISliderCustom : UISlider
{

    public UISliderCustom(IntPtr handle) : base(handle) { }

    public UISliderCustom()
    {
        // Called when created from code.
        Initialize();
    }

    public override void AwakeFromNib()
    {
        // Called when loaded from xib or storyboard.
        Initialize();
    }

    void Initialize()
    {
        // Common initialization code here.

        var longPress = new UILongPressGestureRecognizer(tapAndSlide);
        longPress.MinimumPressDuration = 0;
        //longPress.CancelsTouchesInView = false;
        this.AddGestureRecognizer(longPress);
        this.UserInteractionEnabled = true;

    }

    private void tapAndSlide(UILongPressGestureRecognizer gesture)
    {
        System.Diagnostics.Debug.WriteLine($"{nameof(UISliderCustom)} RecognizerState {gesture.State}");

        // need to propagate events down the chain
        // I imagine iOS does something similar
        // for whatever recogniser on the thumb control
        // It's not enough to set CancelsTouchesInView because
        // if clicking on the track away from the thumb control
        // the thumb gesture recogniser won't pick it up anyway
        switch (gesture.State)
        {
            case UIGestureRecognizerState.Cancelled:
                this.SendActionForControlEvents(UIControlEvent.TouchCancel);
                break;

            case UIGestureRecognizerState.Began:
                this.SendActionForControlEvents(UIControlEvent.TouchDown);
                break;

            case UIGestureRecognizerState.Changed:
                this.SendActionForControlEvents(UIControlEvent.ValueChanged);                    
                break;

            case UIGestureRecognizerState.Ended:
                this.SendActionForControlEvents(UIControlEvent.TouchUpInside);
                break;

            case UIGestureRecognizerState.Failed:
                //?
                break;

            case UIGestureRecognizerState.Possible:
                //?
                break;

        }

        var pt = gesture.LocationInView(this);
        var thumbWidth = CurrentThumbImage.Size.Width;
        var value = 0f;

        if (pt.X <= thumbWidth / 2)
        {
            value = this.MinValue;
        }
        else if (pt.X >= this.Bounds.Size.Width - thumbWidth / 2)
        {
            value = this.MaxValue;
        }
        else
        {
            var percentage = ((pt.X - thumbWidth / 2) / (this.Bounds.Size.Width - thumbWidth));
            var delta = percentage * (this.MaxValue - this.MinValue);
            value = this.MinValue + (float)delta;
        }

        if (gesture.State == UIGestureRecognizerState.Began)
        {               
            UIView.Animate(0.35, 0, UIViewAnimationOptions.CurveEaseInOut,
                () =>
                {
                    this.SetValue(value, true);
                },
                null);
        }
        else
        {
            this.SetValue(value, animated: false);
        }

    }

}
Musketeer answered 4/9, 2018 at 11:9 Comment(0)
A
0

From Apple,

https://developer.apple.com/forums/thread/108317

Now this works fine on iOS 10 and iOS 11. You can slide as usual and thanks to the above code you can tap on slider and it slides automatically. However in iOS 12 this doesn't work. You have to force touch on it for tap to work

Abstruse answered 19/8, 2020 at 10:48 Comment(0)
R
0

Here is my solution that works :

import UIKit

class CustomSlider: UISlider {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
    
    
    private func setupView() {
        addTapGesture()
    }
    
    private func addTapGesture() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        addGestureRecognizer(tap)
    }
    
    @objc private func handleTap(_ sender: UITapGestureRecognizer) {
        let location = sender.location(in: self)
        let percent = minimumValue + Float(location.x / bounds.width) * maximumValue
        setValue(percent, animated: true)
        sendActions(for: .valueChanged)
    }
}
Rebut answered 9/11, 2020 at 11:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.