8

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)
Matt Le Fleur
  • 2,708
  • 29
  • 40
Rawan
  • 1,589
  • 4
  • 23
  • 47
  • 1
    Could you show some code how you're using it? I've used this before with no problems. – Michal Jul 02 '15 at 08:52
  • yes for sure. I'm trying to add it programmatically like this: var rangeSlider = NMRangeSlider(frame: CGRectMake(16, 6, 275, 34)) rangeSlider.lowerValue = 0.54 rangeSlider.upperValue = 0.94 self.view.addSubview(rangeSlider) – Rawan Jul 02 '15 at 08:56
  • @Michal excuse me, when i saw the demo, i noticed : to implement the range slider what we do is just to make the view class is NMRangeSlider. Right ? and did you use this framework with swift ? or Obj-C – Rawan Jul 02 '15 at 08:58
  • See my answer now - you need to set all the views for the components of the slider. Then it will work. (tested) – Michal Jul 02 '15 at 09:31

4 Answers4

6

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

Brian Bird
  • 1,176
  • 18
  • 21
  • in the `viewDidLoad` you should insert `rangeSlider.addTarget(self, action: #selector(rangeSliderValueChanged), for: .valueChanged)`so that moves of the cursors are detected – J Kasparian Mar 03 '22 at 15:38
  • Nice solution. When used in a modalView, however, I had cross-talks between the rangeSlider and the gestureRecognizer managing the dismissal of the modalView, so that I had to temporarily inhibit the latter, following this answer https://stackoverflow.com/a/59462475/17265403 and my comment to it. – J Kasparian Mar 05 '22 at 13:42
2

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.

Community
  • 1
  • 1
Michal
  • 15,429
  • 10
  • 73
  • 104
  • Try entering the `Debug View Hierarchy` and see if it's on canvas. – Michal Jul 02 '15 at 09:44
  • And is it all see through (invisible) or white or what does it look like? – Michal Jul 02 '15 at 11:10
  • all the components of the slider are white !! it seems that it didn't take the default values that the slider provide it!!! – Rawan Jul 02 '15 at 11:15
  • i wander why that happen !! in the demo of NMRangSlider, the just change the class of the view to NMRangeSlide, then alll it works !! i feel so bad really :| – Rawan Jul 02 '15 at 11:16
  • They shouldn't be white if you copy and paste my code I put in my answer. – Michal Jul 02 '15 at 11:17
  • Also try changing the color of your views background (`self.view.backgroundColor = .grayColor()` for example) and maybe you'll see it where it is. – Michal Jul 02 '15 at 11:18
  • this is a snapshot for it : https://cloud.githubusercontent.com/assets/12219139/8475614/1faadb7c-20c2-11e5-8217-1671ff5416cc.png – Rawan Jul 02 '15 at 11:18
  • My code does not create that - try the code that is in my answer (it changed) – Michal Jul 02 '15 at 11:19
  • yeah i tried the new code, the slider appear with two white thumb, but the trakImage doesn't appear even if i assign an image to it. but can you tell me why if i hint this code: rangeSlider.lowerHandleImageNormal = lowerThumb.pb_takeSnapshot() rangeSlider.lowerHandleImageHighlighted = lowerThumbHigh.pb_takeSnapshot() rangeSlider.upperHandleImageNormal = lowerThumb.pb_takeSnapshot() rangeSlider.upperHandleImageHighlighted = lowerThumbHigh.pb_takeSnapshot() the slider will disappear again ?! – Rawan Jul 02 '15 at 11:26
  • My bad, the CGContext doesn't like the blackColor, try changing this line `bgImage.backgroundColor = .blackColor()` to `.greenColor()` to see if it works. – Michal Jul 02 '15 at 11:30
  • It disappears because it does not know what to display, since you're not assigning it. There are *no default values* - you have to set everything yourself. If nothing happened, then you're doing something different. I'm building the code **AS-IS** in Xcode 6.4, iPhone 6 Simulator against SDK 8.4 and I'm getting the slider with green background. – Michal Jul 02 '15 at 11:40
0

try this code :

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,
        width: width, height: 31.0)
}
Jay Bhalani
  • 4,142
  • 8
  • 37
  • 50
0

I implemented the range slider using : https://github.com/Zengzhihui/RangeSlider

In the GZRangeSlider class, there is a method called : private func setLabelText()

In that method, just put :

leftTextLayer.frame = CGRectMake(leftHandleLayer.frame.minX - 0.5 * (kTextWidth - leftHandleLayer.frame.width), leftHandleLayer.frame.minY - kTextHeight, kTextWidth, kTextHeight)

rightTextLayer.frame = CGRectMake(rightHandleLayer.frame.minX - 0.5 * (kTextWidth - leftHandleLayer.frame.width), leftTextLayer.frame.minY, kTextWidth, kTextHeight)

to animate the lower and upper labels..

This one is working well for me and its in swift.. just try it..

SymbolixAU
  • 25,502
  • 4
  • 67
  • 139