11

The default behaviour of UISlider is that its thumb isn't centered on start/end of track. Like below:

default UISlider

I would like to modify it's behaviour to get:

desired slider

where thumb's center can be positioned on start or end.

I have tried to cover start/end with empty UIView. Effect is that is look almost ok, but thumb has dropshadow that reveals my hack in some positions (which I can live with), however my hacky view covers a little the thumb and captures it's touch event (even when user interactions are disabled).

So I guess I would need to pack my hacky view between thumb and track. Possibly without manipulating its internal views. Any ideas?

Fishman
  • 1,737
  • 1
  • 25
  • 42
  • Hi, now i face the same issue, but different UI, without thumb image (give thumb color to clear in xib). now if i set slider value to 0, it ll minimum 4 to 6 pixel for minimum value, i can't set zero as well as max value. please help me if you found solution. – Ammaiappan May 02 '17 at 17:21
  • Hi, how can you add the vertical line for steps in both start and end of slider? I tried to add minTrackImage/maxTrackImage but they have a big space to the track. Can you share the solution? – Bad_Developer Jul 21 '21 at 18:38

3 Answers3

10

UISlider class has a method thumbRect(forBounds:trackRect:value:) that you can override to control the drawing rectangle for the slider’s thumb image. So assuming that this space is 5 points (might vary depending on the thumb image you are using) we can do the following:

  1. Subclass UISlider and override thumbRect(forBounds:trackRect:value:) to hook ourselves into the process
  2. Define the thumbSpace (I assumed it is 5), and from the thumbSpace we can calculate starting offset (5 points before the slider) and ending offset (5 points after the slider).
  3. Calculate the thumb translation in relation to current value, minValue and maxValue (In other words, based on the current value, we are shifting the thumb a little to cover the spaces we don't want to show)
  4. Return a shifted rectangle (after applying the x translation) to the super method to do the regular calculations

The complete code should be like the following:

class CustomSlider: UISlider {
    let defaultThumbSpace: Float = 5
    lazy var startingOffset: Float = 0 - defaultThumbSpace
    lazy var endingOffset: Float = 2 * defaultThumbSpace

    override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
        let xTranslation =  startingOffset + (minimumValue + endingOffset) / maximumValue * value
        return super.thumbRect(forBounds: bounds,
                               trackRect: rect.applying(CGAffineTransform(translationX: CGFloat(xTranslation),
                                                                          y: 0)),
                               value: value)
    }
}
Mostafa Abdellateef
  • 1,145
  • 14
  • 12
  • 1
    It's working great when sliding, but when you grab the thumb again close to one limit it jumps to another value pretty fast, any clue to solve that? – cyril94440 May 14 '21 at 18:40
5

Swift 5 version for lazy ones like me :)

class CustomSlider: UISlider {

    private let trackHeight: CGFloat = 8

    override func trackRect(forBounds bounds: CGRect) -> CGRect {
        let point = CGPoint(x: bounds.minX, y: bounds.midY)
        return CGRect(origin: point, size: CGSize(width: bounds.width, height: trackHeight))
    }

    private let thumbWidth: Float = 52
    lazy var startingOffset: Float = 0 - (thumbWidth / 2)
    lazy var endingOffset: Float = thumbWidth

    override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
        let xTranslation =  startingOffset + (minimumValue + endingOffset) / maximumValue * value
        return super.thumbRect(forBounds: bounds, trackRect: rect.applying(CGAffineTransform(translationX: CGFloat(xTranslation), y: 0)), value: value)
    }
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
ergunkocak
  • 3,334
  • 1
  • 32
  • 31
-1

Mostafa's and ergunkocak's solutions were failing for me when the minimum value was < 0. This is what I ended up using:

class CustomSlider: UISlider {
    var customThumbSpacing: CGFloat?

    override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
        if let customThumbSpacing = customThumbSpacing {
            let minOrigin = super.thumbRect(forBounds: bounds, trackRect: rect, value: minimumValue).origin.x
            let maxOrigin = super.thumbRect(forBounds: bounds, trackRect: rect, value: maximumValue).origin.x
            let defaultFrame = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
            
            let newOrigin = defaultFrame.origin.x.map(
                from: minOrigin...maxOrigin,
                to: minOrigin-customThumbSpacing...maxOrigin+customThumbSpacing)
                        
            return CGRect(x: newOrigin,
                          y: defaultFrame.origin.y,
                          width: defaultFrame.width,
                          height: defaultFrame.height)
        } else {
            return super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
        }
    }
}

extension CGFloat {
    func map(from: ClosedRange<CGFloat>, to: ClosedRange<CGFloat>) -> CGFloat {
        let result = ((self - from.lowerBound) / (from.upperBound - from.lowerBound)) * (to.upperBound - to.lowerBound) + to.lowerBound
        return result
    }
}
John Scalo
  • 3,194
  • 1
  • 27
  • 35