107

How to detect the event when the user has ended the drag of a slider pointer?

Alex Cio
  • 5,752
  • 4
  • 40
  • 73
DroidHeaven
  • 2,173
  • 3
  • 22
  • 31

16 Answers16

187

If you don't need any data inbetween drag, than you should simply set:

[mySlider setContinuous: NO];

This way you will receive valueChanged event only when the user stops moving the slider.

Swift 5 version:

mySlider.isContinuous = false
Nikita
  • 389
  • 3
  • 9
Rok Jarc
  • 18,235
  • 9
  • 64
  • 120
134

You can add an action that takes two parameters, sender and an event, for UIControlEventValueChanged:

[slider addTarget:self action:@selector(onSliderValChanged:forEvent:) forControlEvents:UIControlEventValueChanged]

Then check the phase of the touch object in your handler:

- (void)onSliderValChanged:(UISlider*)slider forEvent:(UIEvent*)event {     
    UITouch *touchEvent = [[event allTouches] anyObject];
    switch (touchEvent.phase) {     
        case UITouchPhaseBegan:
            // handle drag began
            break;
        case UITouchPhaseMoved:
            // handle drag moved
            break;
        case UITouchPhaseEnded:
            // handle drag ended
            break;
        default:
            break;
    }
}

Swift 4 & 5

slider.addTarget(self, action: #selector(onSliderValChanged(slider:event:)), for: .valueChanged)
@objc func onSliderValChanged(slider: UISlider, event: UIEvent) {
    if let touchEvent = event.allTouches?.first {
        switch touchEvent.phase {
        case .began:
            // handle drag began
        case .moved:
            // handle drag moved
        case .ended:
            // handle drag ended
        default:
            break
        }
    }
}

Note in Interface Builder when adding an action you also have the option to add both sender and event parameters to the action.

Devin Pitcher
  • 1,984
  • 1
  • 14
  • 10
  • 1
    Works perfectly! Where did you get the source for that? – Roi Mulia Aug 29 '16 at 07:06
  • 2
    thanks @RoiMulia, I don't remember where I first saw it, I've been using in sliders for years. I sometimes use it to ignore the change on UITouchPhaseEnded since the value tends to jump when the user lifts their finger. – Devin Pitcher Aug 30 '16 at 03:49
  • Is this a robust way to handle it? What if there are multiple events in `allTouches` which belong to different phases? – David Mar 09 '17 at 23:03
  • @David UISlider has multipleTouchEnabled = NO by default. – Devin Pitcher Mar 10 '17 at 04:48
  • 2
    Remember to set `isContinuous = true` in your `UISlider`. – krlbsk Sep 20 '17 at 07:24
  • I wrote some code in case : .move and .end, now what happen is slider thumb Image is not moving smoothly.With out any stuff in .move and , .end slider is moving smoothly. – Malleswari Jan 04 '18 at 12:36
  • @Malleswari as you move the slider the .moved case will gets called continuously (i.e. many many times) on the main thread as the slider position updates. So you don't want to do any processor or I/O intensive work in the .move case. – Devin Pitcher Jan 05 '18 at 00:37
  • But my slider is not smooth. I am calling slider like this progressSlider.addTarget(self, action: #selector(sliderTouchEvents), for: UIControlEvents.valueChanged) – Malleswari Jan 05 '18 at 06:48
  • @Malleswari what are you doing inside the .move case? You shouldn't do anything that affects the state of the slider or that takes more than a few milliseconds to process. – Devin Pitcher Jan 05 '18 at 16:00
  • 2
    This is a great solution, however it has an issue with touch cancelling. Despite there being a "cancelled" event in the touchEvent enum, it is not fired when the touch is cancelled. Therefore I recommend also adding a listener for the touchCancel event from the Slider. This should cover all possibilities. – bnussey Jul 19 '18 at 19:30
  • @Malleswari You can declare this: private var renderTimer = Timer() And then use where you need it as: self.renderTimer = Timer( timeInterval: 0.1, target: self, selector: #selector(AudioMessageCell.renderTimerCycle), userInfo: nil, repeats: true ) Where you can actually change timeInterval, lower it to 0,01 for example, to get your slider moving smoother. – timetraveler90 Jun 18 '19 at 12:23
  • 2
    I saw that sometimes the phase can go: began, stationary, moved, but then never ended nor cancelled. This is problematic for my use case as I expected every interaction to end or be canceled. The fix is noted above: use a separate touch cancel target/action. – Jordan H Jan 28 '20 at 04:00
  • 1
    Also note that `allTouches` will be `nil` when incrementing/decrementing the value with VoiceOver. – Jordan H Jan 28 '20 at 04:49
  • @DevinPitcher This is by far the most extensive solution I could get. Amazing work. – Hitit Sep 24 '20 at 01:16
71

I use the "Touch Up Inside" and "Touch up outside" notifications.

Interface Builder:

Connect both notifications in the Interface Builder to your receiving method. The method could look like this:

- (IBAction)lengthSliderDidEndSliding:(id)sender {
    NSLog(@"Slider did end sliding... Do your stuff here");
}

In code:

If you want to wire it programatically you would have something like this in your viewWillAppear (or wherever it fits you) call:

[_mySlider addTarget:self
              action:@selector(sliderDidEndSliding:) 
    forControlEvents:(UIControlEventTouchUpInside | UIControlEventTouchUpOutside)];

The receiving method would look like this:

- (void)sliderDidEndSliding:(NSNotification *)notification {
     NSLog(@"Slider did end sliding... Do your stuff here");
}
thomas
  • 1,149
  • 6
  • 18
19

Swift 5 and Swift 4

  1. Add target for touchUpInside and touchUpOutside events. You can do it either programatically or from storyboard. This is how you do it programatically

    slider.addTarget(self, action: #selector(sliderDidEndSliding), for: [.touchUpInside, .touchUpOutside])
    
  2. Write your function to handle the end of slider drag

    func sliderDidEndSliding() {
        print("end sliding")
    }
    
Guy Daher
  • 5,314
  • 5
  • 36
  • 64
19

Since UISlider is a subclass of UIControl, you can set a target and action for its UIControlEventTouchUpInside.

If you want to do it in code, it looks like this:

[self.slider addTarget:self action:@selector(dragEndedForSlider:)
    forControlEvents:UIControlEventTouchUpInside];

That will send you a dragEndedForSlider: message when the touch ends.

If you want to do it in your nib, you can control-click your slider to get its connections menu, and then drag from the “Touch Up Inside” socket to the target object.

You should also add a target and action for UIControlEventTouchUpOutside and for UIControlEventTouchCancel.

rob mayoff
  • 342,380
  • 53
  • 730
  • 766
  • 1
    @rob mayoff Sorry but you do need to use UIControlEventTouchUpOutside. Otherwise the selector won't be called if your finger is not on slider when you finish dragging. – user3164248 Apr 11 '15 at 07:15
  • 2
    The behavior of `UISlider` has changed since I wrote this answer. – rob mayoff Apr 11 '15 at 15:29
  • Can confirm that you *do* need to also register a handler for UIControlEventTouchUpOutside in iOS 9. – lms Jun 09 '16 at 14:40
  • I was never able to invoke `UIControlEventTouchCancel` handler in iOS 9. But I'm able to invoke `UIControlEventTouchUpInside` and `UIControlEventTouchUpOutside` only. – petrsyn Sep 29 '16 at 13:36
9

You can use:

- (void)addTarget:(id)target action:(SEL)action 
                   forControlEvents:(UIControlEvents)controlEvents  

to detect when the touchDown and touchUp events occur in UISlider

Mundi
  • 77,414
  • 17
  • 110
  • 132
Mudit Bajpai
  • 2,950
  • 15
  • 34
7

I think you need the control event UIControlEventTouchUpInside.

Pang
  • 8,605
  • 144
  • 77
  • 113
scorpiozj
  • 2,605
  • 4
  • 31
  • 59
5

Swift 3 answer

I had same problem and eventually got this to work, so maybe it will help somebody. Connect your_Slider to 'Value Changed' in storyboard and when slider moved "Slider Touched" actioned and when let go "Slider Released".

@IBAction func your_Slider(_ sender: UISlider) {
    if (yourSlider.isTracking) {
        print("Slider Touched")
    } else {
        print("Slider Released")
    }
}
Andy
  • 87
  • 1
  • 7
4

Connect Touch Up Inside and Value Changed

enter image description here

Abo3atef
  • 2,497
  • 1
  • 31
  • 29
4

Swift 3.1

Sometimes you will want the slider's drag events to fire continuously, but have the option of executing different code depending on whether the slider is still being dragged or has just finished being dragged.

To do this, I've expanded on Andy's answer above that makes use of the slider's isTracking property. I needed to record two states: sliderHasFinishedTracking and sliderLastStoredValue.

My code correctly:

  • doesn't fire an action upon starting drag

  • fires the action if the user only nudges the slider with a single tap (meaning that isTracking remains false)


class SliderExample: UIViewController {
  var slider: UISlider = UISlider()
  var sliderHasFinishedTracking: Bool = true
  var sliderLastStoredValue: Float!

  override func loadView() {
    super.loadView()

    slider.minimumValue = 100.0
    slider.maximumValue = 200.0
    slider.isContinuous = true
    slider.value = 100.0
    self.sliderLastStoredValue = slider.value
    slider.addTarget(self, action: #selector(respondToSlideEvents), for: .valueChanged)
    // add your slider to the view however you like
  }

  func respondToSlideEvents(sender: UISlider) {
    let currentValue: Float = Float(sender.value)
    print("Event fired. Current value for slider: \(currentValue)%.")

    if(slider.isTracking) {
      self.sliderHasFinishedTracking = false
      print("Action while the slider is being dragged ⏳")
    } else {
      if(self.sliderHasFinishedTracking && currentValue == self.sliderLastStoredValue){
        print("Slider has finished tracking and its value hasn't changed, " +
              "yet event was fired anyway. ") // No need to take action.
      } else {
        self.sliderHasFinishedTracking = true
        print("Action upon slider drag/tap finishing ⌛️")
      }
    }

    self.sliderLastStoredValue = currentValue
  }
}
Jamie Birch
  • 4,408
  • 1
  • 32
  • 50
4

In Swift 4 you can use this function

1 Frist step:- Adding the target in slider

self.distanceSlider.addTarget(self, action: #selector(self.sliderDidEndSliding(notification:)), for: ([.touchUpInside,.touchUpOutside]))

2 Second step:- Create a function

@objc func sliderDidEndSliding(notification: NSNotification)
{
   print("Hello-->\(distanceSlider.value)")
}
mike_t
  • 2,198
  • 2
  • 17
  • 34
pansora abhay
  • 673
  • 7
  • 14
2

Maybe you should add target for UIControlEventTouchUpInside、UIControlEventTouchUpOutside、UIControlEventTouchCancel events.

I met the same problem before , because i don't add target for UIControlEventTouchCancel event.

zhuoming
  • 17
  • 1
1

I had similar issue recently. Here is what I did in Swift:

    theSlider.continuous = false;

The code that holds this continuous value reads:

public var continuous: Bool // if set, value change events are generated
    // any time the value changes due to dragging. default = YES

The default seems to be YES (true). Setting it it to false does the thing you want.

Evdzhan Mustafa
  • 3,126
  • 1
  • 20
  • 37
0

RxSwift:

self.slider.rx.controlEvent([.touchUpInside, .touchUpOutside])
    .bind(to: self.viewModel.endSlidingSlider)
    .disposed(by: self.disposeBag)
Adam Smaka
  • 4,239
  • 2
  • 38
  • 38
0

Create an action and outlet for the slider (or you can typecast sender from action to UISlider), and in the UI Uncheck the Continuous Updates check box. This way we will get the slider value only when slider stops dragging.

@IBAction func actionSlider(_ sender: Any) {
   let value = sliderSpeed.value.rounded()
}

enter image description here

0

Try like this

customSlider.minimumValue = 0.0;
    customSlider.maximumValue = 10.0;
[customSlider addTarget:self action:@selector(changeSlider:) forControlEvents:UIControlEventValueChanged];


-(void)changeSlider:(id)sender{
    UISlider *slider=(UISlider *)sender;
    float sliderValue=(float)(slider.value );
NSLog(@"sliderValue = %f",sliderValue);
}
Raj Subbiah
  • 1,314
  • 11
  • 20