Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement range slider in Swift

I'm trying to implement Range Slider and I used custom control called NMRangeSlider.

But when I use it, the slider doesn't appear at all. Could it be also because it's all written in Objective-C?

This is how I've currently implemented it:

var rangeSlider = NMRangeSlider(frame: CGRectMake(16, 6, 275, 34))
rangeSlider.lowerValue = 0.54
rangeSlider.upperValue = 0.94
self.view.addSubview(rangeSlider)
like image 530
Rawan Avatar asked Jul 02 '15 08:07

Rawan


People also ask

How to use Range slider in Swift?

To use SwiftRangeSlider on a storyboard, add a UIView to your view controller and set its class to RangeSlider. The Range Slider defaults to an iOS-esque style of slider, but it can be modified to look lots of different ways!

What is range slider?

A range slider is an input field that merchants can use to select a numeric value within a given range (minimum and maximum values).

How do I add a slider in SwiftUI?

Implementing a normal Slider is as easy as initiating a normal SwiftUI view. All you need is a state value to store the current value of the slider and a range inside this range the slider's value will be changed according to the thumb's relative position to start point.


2 Answers

To create a custom Range Slider I found a good solution here: range finder tutorial iOS 8 but I needed this in swift 3 for my project. I updated this for Swift 3 iOS 10 here:

  1. in your main view controller add this to viewDidLayOut to show a range slider.

    override func viewDidLayoutSubviews() {
       let margin: CGFloat = 20.0
       let width = view.bounds.width - 2.0 * margin
       rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length + 170, width: width, height: 31.0)
    }
    
  2. create the helper function to print slider output below viewDidLayoutSubviews()

    func rangeSliderValueChanged() { //rangeSlider: RangeSlider
        print("Range slider value changed: \(rangeSlider.lowerValue) \(rangeSlider.upperValue) ")//(\(rangeSlider.lowerValue) \(rangeSlider.upperValue))
    }
    
  3. Create the file RangeSlider.swift and add this to it:

    import UIKit
    
    import QuartzCore
    
    class RangeSlider: UIControl {
    
     var minimumValue = 0.0
     var maximumValue = 1.0
     var lowerValue = 0.2
     var upperValue = 0.8
    
     let trackLayer = RangeSliderTrackLayer()//= CALayer() defined in RangeSliderTrackLayer.swift
     let lowerThumbLayer = RangeSliderThumbLayer()//CALayer()
     let upperThumbLayer = RangeSliderThumbLayer()//CALayer()
     var previousLocation = CGPoint()
    
     var trackTintColor = UIColor(white: 0.9, alpha: 1.0)
     var trackHighlightTintColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0)
     var thumbTintColor = UIColor.white
    
     var curvaceousness : CGFloat = 1.0
    
     var thumbWidth: CGFloat {
       return CGFloat(bounds.height)
     }
    
     override init(frame: CGRect) {
       super.init(frame: frame)
    
       trackLayer.rangeSlider = self
       trackLayer.contentsScale = UIScreen.main.scale
       layer.addSublayer(trackLayer)
    
       lowerThumbLayer.rangeSlider = self
       lowerThumbLayer.contentsScale = UIScreen.main.scale
       layer.addSublayer(lowerThumbLayer)
    
       upperThumbLayer.rangeSlider = self
       upperThumbLayer.contentsScale = UIScreen.main.scale
       layer.addSublayer(upperThumbLayer)
    
      }
    
     required init?(coder: NSCoder) {
       super.init(coder: coder)
     }
    
     func updateLayerFrames() {
       trackLayer.frame = bounds.insetBy(dx: 0.0, dy: bounds.height / 3)
       trackLayer.setNeedsDisplay()
    
       let lowerThumbCenter = CGFloat(positionForValue(value: lowerValue))
    
       lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0,
                                   width: thumbWidth, height: thumbWidth)
       lowerThumbLayer.setNeedsDisplay()
    
       let upperThumbCenter = CGFloat(positionForValue(value: upperValue))
       upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0,
                                   width: thumbWidth, height: thumbWidth)
       upperThumbLayer.setNeedsDisplay()
     }
    
     func positionForValue(value: Double) -> Double {
      return Double(bounds.width - thumbWidth) * (value - minimumValue) /
        (maximumValue - minimumValue) + Double(thumbWidth / 2.0)
     }
    
      override var frame: CGRect {
       didSet {
          updateLayerFrames()
        }
      }
    
       override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
          previousLocation = touch.location(in: self)
    
          // Hit test the thumb layers
          if lowerThumbLayer.frame.contains(previousLocation) {
            lowerThumbLayer.highlighted = true
          } else if upperThumbLayer.frame.contains(previousLocation) {
            upperThumbLayer.highlighted = true
          }
    
           return lowerThumbLayer.highlighted || upperThumbLayer.highlighted
       }
    
       func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double {
         return min(max(value, lowerValue), upperValue)
       }
    
       override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
         let location = touch.location(in: self)
    
         // 1. Determine by how much the user has dragged
         let deltaLocation = Double(location.x - previousLocation.x)
         let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - thumbWidth)
    
          previousLocation = location
    
          // 2. Update the values
         if lowerThumbLayer.highlighted {
            lowerValue += deltaValue
            lowerValue = boundValue(value: lowerValue, toLowerValue: minimumValue, upperValue: upperValue)
        } else if upperThumbLayer.highlighted {
          upperValue += deltaValue
          upperValue = boundValue(value: upperValue, toLowerValue: lowerValue, upperValue: maximumValue)
        }
    
        // 3. Update the UI
        CATransaction.begin()
        CATransaction.setDisableActions(true)
    
        updateLayerFrames()
    
        CATransaction.commit()
    
         sendActions(for: .valueChanged)
    
          return true
     }
    
      override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        lowerThumbLayer.highlighted = false
        upperThumbLayer.highlighted = false
      }
    }
    
  4. Next add the thumb layer subclass file RangeSliderThumbLayer.swift and add this to it:

      import UIKit
    
      class RangeSliderThumbLayer: CALayer {
         var highlighted = false
         weak var rangeSlider: RangeSlider?
    
      override func draw(in ctx: CGContext) {
        if let slider = rangeSlider {
          let thumbFrame = bounds.insetBy(dx: 2.0, dy: 2.0)
          let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0
          let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius)
    
          // Fill - with a subtle shadow
          let shadowColor = UIColor.gray
          ctx.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: 1.0, color: shadowColor.cgColor)
          ctx.setFillColor(slider.thumbTintColor.cgColor)
          ctx.addPath(thumbPath.cgPath)
          ctx.fillPath()
    
        // Outline
        ctx.setStrokeColor(shadowColor.cgColor)
        ctx.setLineWidth(0.5)
        ctx.addPath(thumbPath.cgPath)
        ctx.strokePath()
    
        if highlighted {
            ctx.setFillColor(UIColor(white: 0.0, alpha: 0.1).cgColor)
            ctx.addPath(thumbPath.cgPath)
            ctx.fillPath()
        }
      }
     }
    }
    
  5. Finally add the track layer subclass file RangeSliderTrackLayer.swift and add the following to it:

     import Foundation
     import UIKit
     import QuartzCore
    
     class RangeSliderTrackLayer: CALayer {
      weak var rangeSlider: RangeSlider?
    
      override func draw(in ctx: CGContext) {
        if let slider = rangeSlider {
          // Clip
          let cornerRadius = bounds.height * slider.curvaceousness / 2.0
          let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
          ctx.addPath(path.cgPath)
    
          // Fill the track
          ctx.setFillColor(slider.trackTintColor.cgColor)
          ctx.addPath(path.cgPath)
          ctx.fillPath()
    
          // Fill the highlighted range
          ctx.setFillColor(slider.trackHighlightTintColor.cgColor)
          let lowerValuePosition =  CGFloat(slider.positionForValue(value: slider.lowerValue))
          let upperValuePosition = CGFloat(slider.positionForValue(value: slider.upperValue))
          let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height)
        ctx.fill(rect)
       }
      }
     }
    

Build Run and Get: enter image description here

like image 88
Brian Avatar answered Sep 21 '22 20:09

Brian


UPDATE:

It did not show to me, because it was all white. So the solution, without using any other framework and sticking with this one - you need to set all the views for all the components and then it will display well:

enter image description here


I have tried to import it in Swift as I used it before in Objective-C code, but without any luck. If I set everything properly and add it either in viewDidLoad() or viewDidAppear(), nothing gets displayed. One thing is worth mentioning, though - when I enter View Debug Hierarchy, the slider actually is there on the canvas:

enter image description here

But it's simply not rendered with all the colors that I did set before adding in it to the view. For the record - this is the code I used:

override func viewDidAppear(animated: Bool) {
    var rangeSlider = NMRangeSlider(frame: CGRectMake(50, 50, 275, 34))
    rangeSlider.lowerValue = 0.54
    rangeSlider.upperValue = 0.94
    
    let range = 10.0
    let oneStep = 1.0 / range
    let minRange: Float = 0.05
    rangeSlider.minimumRange = minRange
    
    let bgImage = UIView(frame: rangeSlider.frame)
    bgImage.backgroundColor = .greenColor()
    rangeSlider.trackImage = bgImage.pb_takeSnapshot()
    
    let trackView = UIView(frame: CGRectMake(0, 0, rangeSlider.frame.size.width, 29))
    trackView.backgroundColor = .whiteColor()
    trackView.opaque = false
    trackView.alpha = 0.3
    rangeSlider.trackImage = UIImage(named: "")
    
    let lowerThumb = UIView(frame: CGRectMake(0, 0, 8, 29))
    lowerThumb.backgroundColor = .whiteColor()
    let lowerThumbHigh = UIView(frame: CGRectMake(0, 0, 8, 29))
    lowerThumbHigh.backgroundColor = UIColor.blueColor()
    
    rangeSlider.lowerHandleImageNormal = lowerThumb.pb_takeSnapshot()
    rangeSlider.lowerHandleImageHighlighted = lowerThumbHigh.pb_takeSnapshot()
    rangeSlider.upperHandleImageNormal = lowerThumb.pb_takeSnapshot()
    rangeSlider.upperHandleImageHighlighted = lowerThumbHigh.pb_takeSnapshot()
    
    self.view.addSubview(rangeSlider)

    self.view.backgroundColor = .lightGrayColor()
}

Using the method for capturing the UIView as UIImage mentioned in this question:

extension UIView {
    func pb_takeSnapshot() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.mainScreen().scale)
        drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
}

Other solution:

You can also try sgwilly/RangeSlider instead, it's written in Swift and therefore you won't even need a Bridging Header.

like image 35
Michal Avatar answered Sep 24 '22 20:09

Michal