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