senty
senty

Reputation: 12847

Tap on UISlider to Set the Value

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?

Upvotes: 29

Views: 20932

Answers (9)

Brad Howes
Brad Howes

Reputation: 124

For me, @pietor's answer only works if the touch moves enough to trigger a value change. This may be true on actual devices, but in the simulator, it does nothing with a single touch event. The code below works better for me, and is less jerky when the thumb moves to the touch location:

class CustomSlider : UISlider
{
  override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    let track = trackRect(forBounds: bounds)
    let thumb = thumbRect(forBounds: bounds, trackRect: track, value: 0.0)
    let pos = touch.location(in: self)
    let normalizedValue = Float((pos.x - track.minX - thumb.width / 2.0) / (track.width - thumb.width))
    self.setValue(minimumValue + (maximumValue - minimumValue) * normalizedValue, animated: true)
    return true
  }
}

Upvotes: 0

matt
matt

Reputation: 534885

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

Upvotes: 0

Septronic
Septronic

Reputation: 1176

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.

Upvotes: 4

Adam
Adam

Reputation: 1913

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

Upvotes: 6

znx
znx

Reputation: 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.

Upvotes: -1

Diogo Souza
Diogo Souza

Reputation: 420

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

Upvotes: 2

PunainenAurinko
PunainenAurinko

Reputation: 77

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.

Upvotes: 0

pteofil
pteofil

Reputation: 4163

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.

Upvotes: 43

myuiviews
myuiviews

Reputation: 1261

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

Upvotes: 48

Related Questions