0

I have a button that I press and a custom alert appears with a normal UISlider inside of it. There are some other views and labels that I have that show the distance etc that are above and behind the slider. What happens is the thumbRect of the slider doesn't slide to well when touched. I have to be very accurate when trying to slide it and it seems buggy. What I want to do is add a clear UIView in front of it (and the other views and labels above it) and have the clear UIView take control of the slider using a UIGestureRecognizer.

Here's the setup so far. The clearView is what I want to use to take control of the slider. Right now it's behind everything and I colored the clearView red just so it's visible for the example

enter image description here

bgView.addSubview(slider) // slider added to bgView
let trackRect = slider.trackRect(forBounds: slider.frame)
let thumbRect = slider.thumbRect(forBounds: slider.bounds, trackRect: trackRect, value: slider.value)
milesLabel.center = CGPoint(x: thumbRect.midX, y: slider.frame.origin.y - 55)
bg.addSubview(milesLabel) // milesLabel added to bgView

// ** all the other views you see are added to the bgView and aligned with the thumbRect's center **

let milesLabelRect = bgView.convert(milesLabel.frame, to: self.view)
let sliderRect = bgView.convert(slider.frame, to: self.view)

let topOfMilesLabel = milesLabelRect.origin.y
let bottomOfSlider = sliderRect.maxY
let distance = bottomOfSlider - topOfMilesLabel

clearView.frame = CGRect(x: milesLabel.frame.minX, y: milesLabel.frame.minY, width: milesLabel.frame.width, height: distance)
bgView.addSubview(clearView) // clearView added to bgView
bgView.insertSubview(clearView, at: 0)

When I slide the slider everything successfully slides with it including the clearView but of course the thumbRect is still in control of everything.

enter image description here

@objc func onSliderValChanged(sender: UISlider, event: UIEvent) {

    slider.value = round(sender.value)
    UIView.animate(withDuration: 0.25, animations: {
        self.slider.layoutIfNeeded()

        let trackRect = sender.trackRect(forBounds: sender.frame)
        let thumbRect = sender.thumbRect(forBounds: sender.bounds, trackRect: trackRect, value: sender.value)
        self.milesLabel.center = CGPoint(x: thumbRect.midX, y: sender.frame.origin.y - 55)

        // ** all the other views are aligned with the thumbRect's center as it slides **

        self.clearView.frame.origin = self.milesLabel.frame.origin
    })
}

The idea is to move the clearView to the front of everything and use that to control the thumbRect (don't forget it's only red for the example).

bgView.bringSubviewToFront(clearView)
// clearView.backgroundColor = UIColor.clear

enter image description here

I tried to use a UIPanGesture and a UILongPressGestureRecognizer to have the clearView take control of the slider but when I slide the slider only the thumbRect slides, the clearView stays still. Look where the clearView (which is red) is and look where the thumbRect is after I slid it to the 10 mile point.

enter image description here

// separately tried a UILongPressGestureRecognizer too
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
clearView.isUserInteractionEnabled = true
clearView.addGestureRecognizer(panGesture)

@objc func draggedView(_ sender: UIPanGestureRecognizer) {

    if slider.isHighlighted { return }

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

If I play around with it enough the clearView sometimes slides with the the thumbRect but they definitely aren't in alignment and it's very buggy. I also lost control of all the other views the were aligned with the thumbRect.

How can I pass control from the slider's thumbRect to the clearView?

Lance Samaria
  • 17,576
  • 18
  • 108
  • 256

2 Answers2

0

You might have your mind set on doing it this way, which is okay. But I have a fast alternative for you that might suit your needs.

If you subclass your playback slider and override the point() method you can increase the surface area of the slider's thumb.

class CustomPlaybackSlider: UISlider {

    // Will increase target surface area for thumb.
    override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        var bounds: CGRect = self.bounds
        // Set the inset to a negative value by how much you want the surface area to increase by.
        bounds = bounds.insetBy(dx: -10, dy: -15)
        return bounds.contains(point)
    }

}

Might simplify things for you. Although, the only downside of this approach is that the surface area for the touch would extend equally down/up for your dx value and equally left/right for your dy value.

Hope this helps.

jake
  • 1,226
  • 8
  • 14
0

I found the key to the problem here in this answer. @PaulaHasstenteufel said

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

What I had to do was

  1. Use a UIPanGestureRecognizer
  2. comment out the UISlider's target action
  3. use the code from the target action's selector method in step-4 instead of using it in @objc func onSliderValChanged(sender: UISlider, event: UIEvent { }
  4. take the code from step-3 and add it to a new function
  5. add the function from step-5 to the bottom of the UIPanGetstureRecognizer's method

1- use the UIPanGestureRecognizer and make userInteraction is enabled for the UIView

let panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
clearView.isUserInteractionEnabled = true
clearView.addGestureRecognizer(panGesture)

2- the Slider, I commented out it's addTarget for the .valueChanged event

let slider: UISlider = {
    let slider = UISlider()
    slider.translatesAutoresizingMaskIntoConstraints = false
    slider.minimumValue = 1
    slider.maximumValue = 5
    slider.value = 1
    slider.isContinuous = true
    slider.maximumTrackTintColor = UIColor(white: 0, alpha: 0.3)

    // ** COMMENT THIS OUT **
    //slider.addTarget(self, action: #selector(onSliderValChanged(sender:event:)), for: .valueChanged)

    return slider
}()

3- Take the code from the selector method the targetAction from above was using and instead insert that code in step 4

@objc func onSliderValChanged(sender: UISlider, event: UIEvent) {

    /*
    use this code in step-4 instead

    slider.value = round(sender.value)

    UIView.animate(withDuration: 0.25, animations: {
        self.slider.layoutIfNeeded()

        let trackRect = sender.trackRect(forBounds: sender.frame)
        let thumbRect = sender.thumbRect(forBounds: sender.bounds, trackRect: trackRect, value: sender.value)
        self.milesLabel.center = CGPoint(x: thumbRect.midX, y: sender.frame.origin.y - 55)

        // ** all the other views are aligned with the thumbRect's center as it slides **

        self.clearView.frame.origin = self.milesLabel.frame.origin
    })
    */
}

4- Create a new method and insert the code from step-3

func sliderValueChanged(_ sender: UISlider, value: Float) {

    // round the value for smooth sliding
    sender.value = round(value)

    UIView.animate(withDuration: 0.25, animations: {
        self.slider.layoutIfNeeded()

        let trackRect = sender.trackRect(forBounds: sender.frame)
        let thumbRect = sender.thumbRect(forBounds: sender.bounds, trackRect: trackRect, value: sender.value)
        self.milesLabel.center = CGPoint(x: thumbRect.midX, y: sender.frame.origin.y - 55)

        // ** all the other views are aligned with the thumbRect's center as it slides **

        self.clearView.frame.origin = self.milesLabel.frame.origin
    })
}

5- I used the same code that I used for the UIGestureRecognizer but commented out the setValue function and instead used the function from step 4

@objc fileprivate func draggedView(_ sender: UIPanGestureRecognizer) {

    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

    // I had to comment this out because the siding was to abrupt
    //slider.setValue(value, animated: true) 

    // ** CALLING THIS FUNCTION FROM STEP-3 IS THE KEY TO GET THE UIView TO CONTROL THE SLIDER **
    sliderValueChanged(slider, value: value)
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256