Search code examples
iosswiftscrolluislider

How to prevent UISlider from jumping into place while snapping/stepping


I have a UISlider that snaps into 5 different places.

slider.minimumValue = 1
slider.maximumValue = 5

The snapping works fine but it's abrupt and doesn't smoothly snap into place. It sort of "jumps" into place

I tried animating the scroll (didn't work):

@objc func onSliderValChanged(_ sender: UISlider) {
    UIView.animate(withDuration: 0.5) {
        self.slider.value = round(sender.value)
        self.slider.setValue(round(sender.value), animated: true)
    }
}

I also tried adding a panGesture (didn't work):

panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureHandler(_:)))
slider.addGestureRecognizer(panGesture)

@objc func panGestureHandler(_ recognizer: UIPanGestureRecognizer){

    let currentPoint = recognizer.location(in: slider)
    let percentage = currentPoint.x/slider.bounds.size.width

    let delta = Float(percentage) * (slider.maximumValue - slider.minimumValue)
    let value = slider.minimumValue + delta
    slider.setValue(round(value), animated: true)
}

How can I achieve the slider to smoothly scroll but still snap into place?

Here's the original code without the above tries:

let slider: UISlider = {
    let slider = UISlider()
    slider.translatesAutoresizingMaskIntoConstraints = false
    slider.minimumValue = 1
    slider.maximumValue = 5
    slider.value = 3
    slider.isContinuous = true
    slider.addTarget(self, action: #selector(onSliderValChanged(_:)), for: .valueChanged)
    return slider
}()

@objc func onSliderValChanged(_ sender: UISlider) {

    slider.value = round(sender.value)
}

func setAnchors() {
    slider.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
    slider.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10).isActive = true
    slider.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true
}

Solution

  • A UISlider draws itself using several UIImageView subviews. When you change the slider's value, the slider sends itself setNeedsLayout, but it doesn't actually change the frames of its subviews until it receives layoutSubviews during the layout phase of the run loop.

    To animate the change in value, you need to make the slider lay out its subviews inside your animation block. Thus:

    @IBAction func sliderValueChanged(_ slider: UISlider) {
        slider.value = round(slider.value)
        UIView.animate(withDuration: 0.2, animations: {
            slider.layoutIfNeeded()
        })
    }