-1

I followed a tutorial on Raywenderlich website (https://www.raywenderlich.com/7595-how-to-make-a-custom-control-tutorial-a-reusable-slider) in order to create a custom Range Slider. The steps I carried out are as follow:

(1) Created a swift file and added the below code to it:

import UIKit

class RangeSlider: UIControl {

    override var frame: CGRect {

        didSet {

            updateLayerFrames()

        }

    }

    private var previousLocation = CGPoint()

    var minimumValue: CGFloat = 0 {

        didSet {

            updateLayerFrames()

        }

    }

    var maximumValue: CGFloat = 1 {

        didSet {

            updateLayerFrames()

        }

    }

    var lowerValue: CGFloat = 0.2 {

        didSet {

            updateLayerFrames()

        }

    }

    var upperValue: CGFloat = 0.8 {

        didSet {

            updateLayerFrames()

        }

    }

    var trackTintColor = UIColor(white: 0.9, alpha: 1) {

        didSet {

            trackLayer.setNeedsDisplay()

        }

    }

    var trackHighlightTintColor = UIColor(red: 0, green: 0.45, blue: 0.94, alpha: 1) {

        didSet {

            trackLayer.setNeedsDisplay()

        }

    }

    var thumbImage = #imageLiteral(resourceName: "Oval") {

        didSet {

            upperThumbImageView.image = thumbImage

            lowerThumbImageView.image = thumbImage

            updateLayerFrames()

        }

    }

    var highlightedThumbImage = #imageLiteral(resourceName: "HighlightedOval") {

        didSet {

            upperThumbImageView.highlightedImage = highlightedThumbImage

            lowerThumbImageView.highlightedImage = highlightedThumbImage

            updateLayerFrames()

        }

    }

    private let trackLayer = RangeSliderTrackLayer()

    private let lowerThumbImageView = UIImageView()

    private let upperThumbImageView = UIImageView()

    override init(frame: CGRect) {

        super.init(frame: frame)

        trackLayer.rangeSlider = self

        trackLayer.contentsScale = UIScreen.main.scale

        layer.addSublayer(trackLayer)

        lowerThumbImageView.image = thumbImage

        addSubview(lowerThumbImageView)

        upperThumbImageView.image = thumbImage

        addSubview(upperThumbImageView)

    }

    required init?(coder aDecoder: NSCoder) {

        fatalError("init(coder:) has not been implemented")

    }

    private func updateLayerFrames() {

        CATransaction.begin()

        CATransaction.setDisableActions(true)

        trackLayer.frame = bounds.insetBy(dx: 0.0, dy: bounds.height / 3)

        trackLayer.setNeedsDisplay()

        lowerThumbImageView.frame = CGRect(origin: thumbOriginForValue(lowerValue),size: thumbImage.size)

        upperThumbImageView.frame = CGRect(origin: thumbOriginForValue(upperValue),size: thumbImage.size)

        CATransaction.commit()

    }

    func positionForValue(_ value: CGFloat) -> CGFloat {

        return bounds.width * value

    }

    private func thumbOriginForValue(_ value: CGFloat) -> CGPoint {

        let x = positionForValue(value) - thumbImage.size.width / 2.0

        return CGPoint(x: x, y: (bounds.height - thumbImage.size.height) / 2.0)

    }

}

extension RangeSlider {

    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {

        previousLocation = touch.location(in: self)

        if lowerThumbImageView.frame.contains(previousLocation) {

            lowerThumbImageView.isHighlighted = true

        } else if upperThumbImageView.frame.contains(previousLocation) {

            upperThumbImageView.isHighlighted = true

        }

        return lowerThumbImageView.isHighlighted || upperThumbImageView.isHighlighted

    }

    override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {

        let location = touch.location(in: self)

        let deltaLocation = location.x - previousLocation.x

        let deltaValue = (maximumValue - minimumValue) * deltaLocation / bounds.width

        previousLocation = location

        if lowerThumbImageView.isHighlighted {

            lowerValue += deltaValue

            lowerValue = boundValue(lowerValue, toLowerValue: minimumValue, upperValue: upperValue)

        } else if upperThumbImageView.isHighlighted {

            upperValue += deltaValue

            upperValue = boundValue(upperValue, toLowerValue: lowerValue, upperValue: maximumValue)

        }

        sendActions(for: .valueChanged)

        return true

    }

    private func boundValue(_ value: CGFloat, toLowerValue lowerValue: CGFloat, upperValue: CGFloat) -> CGFloat {

        return min(max(value, lowerValue), upperValue)

    }

    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {

        lowerThumbImageView.isHighlighted = false

        upperThumbImageView.isHighlighted = false

    }

}

(2) I then created another Swift file inside my project and added the below code inside of it as per the aforementioned tutorial:

import UIKit

class RangeSliderTrackLayer: CALayer {

    weak var rangeSlider: RangeSlider?

    override func draw(in ctx: CGContext) {

        guard let slider = rangeSlider else {

            return

        }

        let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)

        ctx.addPath(path.cgPath)

        ctx.setFillColor(slider.trackTintColor.cgColor)

        ctx.fillPath()

        ctx.setFillColor(slider.trackHighlightTintColor.cgColor)

        let lowerValuePosition = slider.positionForValue(slider.lowerValue)

        let upperValuePosition = slider.positionForValue(slider.upperValue)

        let rect = CGRect(x: lowerValuePosition, y: 0, width: upperValuePosition - lowerValuePosition, height: bounds.height)

        ctx.fill(rect)

    }

}

(3) The last step, was that I created an instance of the custom Range Slider class inside the ViewController I am interested in implementing the RangeSlider inside:

import UIKit

class FilterDataVC: UIViewController {

    let rangeSlider = RangeSlider(frame: .zero)

    override func viewDidLoad() {

        super.viewDidLoad()

        view.addSubview(rangeSlider)

        rangeSlider.addTarget(self, action: #selector(rangeSliderValueChanged(_:)), for: .valueChanged)

    }

    override func viewDidLayoutSubviews() {

        let margin: CGFloat = 20

        let width = view.bounds.width - 2 * margin

        let height: CGFloat = 30

        rangeSlider.frame = CGRect(x: 0, y: 0, width: width, height: height)

        rangeSlider.center = view.center

    }

    @objc func rangeSliderValueChanged(_ rangeSlider: RangeSlider) {

        let values = "(\(rangeSlider.lowerValue) \(rangeSlider.upperValue))"

}

Up to this point, things worked flawlessly and I obtained same results as the tutorial. However, obviously I need to customise the RangeSlider for my own App. Thus, I tried to change the minimumValue, maximumValue, lowerValue and upperValue of the slider by changing the portion of the code stated in Step-01 above, to the one illustrated below:

var minimumValue: CGFloat = 100 {

            didSet {

                updateLayerFrames()

            }

        }

        var maximumValue: CGFloat = 1200 {

            didSet {

                updateLayerFrames()

            }

        }

        var lowerValue: CGFloat = 200 {

            didSet {

                updateLayerFrames()

            }

        }

        var upperValue: CGFloat = 800 {

            didSet {

                updateLayerFrames()

            }

        }

However, after carrying out the above modifications, and ran my App using the simulator. The RangeSlider only displayed the track, I cannot see the thumbs. Any idea what did go wrong?

Thanh Vu
  • 1,599
  • 10
  • 14
devJo
  • 63
  • 7
  • That's a lot of code to scroll through, not to mention all the empty lines. Did you try the debugger for this? – Joakim Danielson Sep 07 '19 at 15:26
  • I was hopping the empty lines will make it easier t read. And the reason I put a lot of code, is due to the fact that this is not a standard controller. Thus, I thought the error might be in the code used to construct the controller itself. – devJo Sep 07 '19 at 15:48
  • My guess is there a division by the width (max value - min value) that is needed to adjust the sliders width to the frame it is in. It might not be an error per se but the thumbnails are far far left and right of the visible area. What happens if you set lowerValue for instance right between min and max, this is 650. Can you see it then? – Joakim Danielson Sep 07 '19 at 16:03
  • I believe you are correct, because when I changed the minimumValue to 0 and the maximumValue to 30. I managed to three the thumbs. however when they slide they do not behave properly. Any idea which bit of code I need to modify to change how much a slide should be equal to? Any in if the user slide one of the thumbs only one tick, how much one tick should be equal to? – devJo Sep 07 '19 at 16:08
  • No direct idea right now, I would debug the code specially the first part where everything is set up – Joakim Danielson Sep 07 '19 at 16:16
  • Thanks a lot Joakim for your constructive advice. I managed to sort out my problem by modifying one of the lines stated in above post to: func positionForValue(_ value: CGFloat) -> CGFloat { return (bounds.width * value) / (maximumValue - minimumValue) } – devJo Sep 07 '19 at 16:22
  • Easier to use one of the free range sliders from github: https://github.com/search?l=Swift&o=desc&q=range+uislider&s=stars&type=Repositories – Yonat Sep 07 '19 at 17:25

1 Answers1

0

I managed to sort out my issue by adjusting portion of the code stated in Step-01 above, by the one mentioned below:

func positionForValue(_ value: CGFloat) -> CGFloat { 

return (bounds.width * value) / (maximumValue - minimumValue) 

} 
devJo
  • 63
  • 7