28

I created a Slider (operating as control of the video, like YouTube has at the bottom) and set the maximum (duration) and minimum values. And then used SeekToTime to change the currentTime. Now, the user can slide the thumb of the slider to change the value

What I want to achieve is letting the user tap on anywhere on the slider and set the current time of the video.

I got an approach from this answer, and I tried to apply it to my case, but couldn't make it work

class ViewController: UIViewController, PlayerDelegate {

var slider: UISlider!

override func viewDidLoad() {
  super.viewDidLoad()

  // Setup the slider
}

func sliderTapped(gestureRecognizer: UIGestureRecognizer) {
  //  print("A")

  let pointTapped: CGPoint = gestureRecognizer.locationInView(self.view)

  let positionOfSlider: CGPoint = slider.frame.origin
  let widthOfSlider: CGFloat = slider.frame.size.width
  let newValue = ((pointTapped.x - positionOfSlider.x) * CGFloat(slider.maximumValue) / widthOfSlider)

  slider.setValue(Float(newValue), animated: true)      
 }
}

I thought this should work, but no luck. Then I tried debugging with trying to printing "A" to logs, but it doesn't so apparently it doesn't go into the sliderTapped() function.

What am I doing wrong? Or is there a better way to achieve what I am trying to achieve?

Community
  • 1
  • 1
senty
  • 12,385
  • 28
  • 130
  • 260

8 Answers8

45

Looks like you need to actually initialize the tap gesture recognizer in your viewDidLoad() per the code example above. There's a comment there, but I don't see the recognizer being created anywhere.

Swift 2:

class ViewController: UIViewController {

    var slider: UISlider!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup the slider

        // Add a gesture recognizer to the slider
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "sliderTapped:")
        self.slider.addGestureRecognizer(tapGestureRecognizer)
    }

    func sliderTapped(gestureRecognizer: UIGestureRecognizer) {
        //  print("A")

        let pointTapped: CGPoint = gestureRecognizer.locationInView(self.view)

        let positionOfSlider: CGPoint = slider.frame.origin
        let widthOfSlider: CGFloat = slider.frame.size.width
        let newValue = ((pointTapped.x - positionOfSlider.x) * CGFloat(slider.maximumValue) / widthOfSlider)

        slider.setValue(Float(newValue), animated: true)      
    }
}

Swift 3:

class ViewController: UIViewController {

    var slider: UISlider!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup the slider

        // Add a gesture recognizer to the slider
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sliderTapped(gestureRecognizer:)))
        self.slider.addGestureRecognizer(tapGestureRecognizer)
    }

    func sliderTapped(gestureRecognizer: UIGestureRecognizer) {
        //  print("A")

        let pointTapped: CGPoint = gestureRecognizer.location(in: self.view)

        let positionOfSlider: CGPoint = slider.frame.origin
        let widthOfSlider: CGFloat = slider.frame.size.width
        let newValue = ((pointTapped.x - positionOfSlider.x) * CGFloat(slider.maximumValue) / widthOfSlider)

        slider.setValue(Float(newValue), animated: true)
    }
}
myuiviews
  • 1,211
  • 1
  • 12
  • 20
  • This answer works so much better than any of the others that I found! Thanks! – kenjikato Oct 13 '16 at 11:19
  • the creation of the `action: "sliderTapped:"` is no longer valid in Xcode 8.2.1. I tried the suggestion Xcode gave me, but it's still failing. Has anyone else figured it out? – Zonker.in.Geneva Mar 18 '17 at 17:23
  • 2
    I think I found it. For Swift 3, the code looks like this: `let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapBlurButton(_:)))` – Zonker.in.Geneva Mar 18 '17 at 18:05
  • 3
    The slider bar has a 15 pixel dead zone on either side of it (not truly a dead zone in that the slider stops moving once it reaches this "dead zone"), so you should increase positionOfSlider.x by 15 and reduce widthOfSlider by 30. Note that this requires accounting for taps that fall outside the bounds of the slider's range of values, i.e. if a user taps in either dead zone. If you are working with slider which "snaps" to integer values, you should use slider.setValue(Float(Int(newValue + 0.5))) or otherwise round the value to the nearest integer. – DivideByZer0 Jul 14 '17 at 07:23
  • 2
    Works great but it does not take into account if the slider has a minimum value other than zero. Here is correct code: let newValue = (((pointTapped.x - positionOfSlider.x) / widthOfSlider) * CGFloat(distanceSlider.maximumValue)) + ((1 - ((pointTapped.x - positionOfSlider.x) / widthOfSlider)) * CGFloat(distanceSlider.minimumValue)) – Paintoshi May 22 '18 at 08:48
  • 1
    Small correction `let pointTapped = gestureRecognizer.location(in: slider.superView)` – Irshad Mohamed Oct 31 '18 at 19:57
40

It seems like just subclassing UISlider and returning always true to the beginTracking produce the desired effect.

iOS 10 and Swift 3

class CustomSlider: UISlider {
    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        return true
    }
}

Use the CustomSlider instead of UISlider afterwards in your code.

pteofil
  • 4,133
  • 17
  • 27
  • 3
    Clean and simple answer. This should now be the accepted answer – Sylvan D Ash May 11 '18 at 14:21
  • 1
    If you have custom code for your slider on finger release for example force discrete steps, then this code will call that function on single tap as well. Maybe that is not desired effect and above answer from myuiviews is needed. In most cases though, this will work just fine! – Paintoshi May 22 '18 at 09:01
  • Finally Apple :) – shelll Jul 31 '18 at 08:02
  • I don't see why this is an answer. I implemented it exactly as described and it didn't cause anything to happen magically when the slider is tapped (except that the value changed event is called). – matt Jan 26 '23 at 02:58
4

I like this approach

extension UISlider {
    public 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)
    }
}

And later just call

let slider = UISlider()
slider.addTapGesture()
Adam
  • 1,776
  • 1
  • 17
  • 28
  • For the calculation of the percent, I would recommend to use this line: `let percent = slider.minimumValue + Float(location.x / slider.bounds.width) * (slider.maximumValue - slider.minimumValue)` – Raoul Nov 12 '22 at 13:18
3

I implemented pteofil's, however, because I already had an action attached to valueChanged, I was having issues with the tracking.

I Was looking at Adam's answer, and I noticed he had made a very good implementation as well, except that I don't generally like adding gesture recognisers. So I combined the two answer (kinda), and now I can both change the value using sliding (and hence triggering the valueChanged as well as if tapped on the slider:

  1. I subclassed UISlider, and overrode the beginTracking method:
    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        return true //Without this, we don't get any response in the method below 
    }
  1. I then overrode touchEnded method (I guess you could override other states as well, but this seemed to be the best logically-suited one)
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let location = touch.location(in: self)
        let conversion = minimumValue + Float(location.x / bounds.width) * maximumValue
        setValue(conversion, animated: false)
        sendActions(for: .valueChanged) //Add this because without this it won't work.
    }

I also have my standard IBAction as well elsewhere (this is not relevant to above directly, but I added it in order to provide the full context):

    @IBAction func didSkipToTime(_ sender: UISlider) {
        print("sender value: \(sender.value)") // Do whatever in this method
    }

Hope it will be of some use and fit your need.

Septronic
  • 1,156
  • 13
  • 32
2

This is my code, based on "myuiviews" answer.
I fixed 2 little "bugs" of the original code.

1 - Tapping on 0 was too difficult, so I made it easier
2 - Sliding the slider's thumb just a little bit was also firing the "tapGestureRecognizer", which makes it return to the initial position, so I added a minimum distance filter to avoid that.

Swift 4

class ViewController: UIViewController {

    var slider: UISlider!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup the slider

        // Add a gesture recognizer to the slider
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sliderTapped(gestureRecognizer:)))
        self.slider.addGestureRecognizer(tapGestureRecognizer)
    }

    @objc func sliderTapped(gestureRecognizer: UIGestureRecognizer) {
        //  print("A")

        var pointTapped: CGPoint = gestureRecognizer.location(in: self.view)
        pointTapped.x -= 30  //Subtract left constraint (distance of the slider's origin from "self.view" 

        let positionOfSlider: CGPoint = slider.frame.origin
        let widthOfSlider: CGFloat = slider.frame.size.width

        //If tap is too near from the slider thumb, cancel
        let thumbPosition = CGFloat((slider.value / slider.maximumValue)) * widthOfSlider
        let dif = abs(pointTapped.x - thumbPosition)
        let minDistance: CGFloat = 51.0  //You can calibrate this value, but I think this is the maximum distance that tap is recognized
        if dif < minDistance { 
            print("tap too near")
            return
        }

        var newValue: CGFloat
        if pointTapped.x < 10 {
            newValue = 0  //Easier to set slider to 0
        } else {
            newValue = ((pointTapped.x - positionOfSlider.x) * CGFloat(slider.maximumValue) / widthOfSlider)
        }



        slider.setValue(Float(newValue), animated: true)
    }
}
Diogo Souza
  • 370
  • 2
  • 12
  • I made a little change for the newValue calculation so as to cater for the slider's minimum value when it's not zero. 'code'newValue = (((pointTapped.x - positionOfSlider.x) * CGFloat(self.offerSliderView.maximumValue - self.offerSliderView.minimumValue) / widthOfSlider) + CGFloat(self.offerSliderView.minimumValue))'code' – Jean-Paul Manuel Dec 06 '17 at 07:47
0

pteofil's answer should be the accepted answer here.

Below is the solution for Xamarin for everyone's interested:

public override bool BeginTracking(UITouch uitouch, UIEvent uievent)
{
    return true;
}

As pteofil mentioned, you need to subclass the UISlider for this to work.

0

So, taking the problem to be a slider whose value can be changed either by sliding the "thumb" or by tapping on the slider as a whole, and pulling together a number of answers and comments already on this page, we can neatly express the solution like this:

class MySlider: UISlider {
    override init(frame: CGRect) {
        super.init(frame:frame)
        config()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        config()
    }

    func config() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
        addGestureRecognizer(tap)
    }

    @objc func tapped(_ tapper: UITapGestureRecognizer) {
        let pointTappedX = tapper.location(in: self).x
        let positionOfSliderX = bounds.origin.x + 15
        let widthOfSlider = bounds.size.width - 30
        let newX = (pointTappedX - positionOfSliderX) / widthOfSlider

        let newValue = newX * CGFloat(maximumValue) + (1 - newX) * CGFloat(minimumValue)
        setValue(Float(newValue), animated: true)
        sendActions(for: .valueChanged)
    }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
-1

The probably simplest solution would be, using the "touch up inside" action, connected trough the interface builder.

@IBAction func finishedTouch(_ sender: UISlider) {

    finishedMovingSlider(sender)
}

This will get called as soon as your finger leaves the phone screen.

znx
  • 1