Uislider That Snaps to a Fixed Number of Steps (Like Text Size in the iOS 7 Settings App)

UISlider that snaps to a fixed number of steps (like Text Size in the iOS 7 Settings app)

Some of the other answers work, but this will give you the same fixed space between every position in your slider. In this example you treat the slider positions as indexes to an array which contains the actual numeric values you are interested in.

@interface MyViewController : UIViewController {
UISlider *slider;
NSArray *numbers;
}
@end

@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
slider = [[UISlider alloc] initWithFrame:self.view.bounds];
[self.view addSubview:slider];

// These number values represent each slider position
numbers = @[@(-3), @(0), @(2), @(4), @(7), @(10), @(12)];
// slider values go from 0 to the number of values in your numbers array
NSInteger numberOfSteps = ((float)[numbers count] - 1);
slider.maximumValue = numberOfSteps;
slider.minimumValue = 0;

// As the slider moves it will continously call the -valueChanged:
slider.continuous = YES; // NO makes it call only once you let go
[slider addTarget:self
action:@selector(valueChanged:)
forControlEvents:UIControlEventValueChanged];
}
- (void)valueChanged:(UISlider *)sender {
// round the slider position to the nearest index of the numbers array
NSUInteger index = (NSUInteger)(slider.value + 0.5);
[slider setValue:index animated:NO];
NSNumber *number = numbers[index]; // <-- This numeric value you want
NSLog(@"sliderIndex: %i", (int)index);
NSLog(@"number: %@", number);
}

I hope that helps, good luck.

Edit: Here's a version in Swift 4 that subclasses UISlider with callbacks.

class MySliderStepper: UISlider {
private let values: [Float]
private var lastIndex: Int? = nil
let callback: (Float) -> Void

init(frame: CGRect, values: [Float], callback: @escaping (_ newValue: Float) -> Void) {
self.values = values
self.callback = callback
super.init(frame: frame)
self.addTarget(self, action: #selector(handleValueChange(sender:)), for: .valueChanged)

let steps = values.count - 1
self.minimumValue = 0
self.maximumValue = Float(steps)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

@objc func handleValueChange(sender: UISlider) {
let newIndex = Int(sender.value + 0.5) // round up to next index
self.setValue(Float(newIndex), animated: false) // snap to increments
let didChange = lastIndex == nil || newIndex != lastIndex!
if didChange {
lastIndex = newIndex
let actualValue = self.values[newIndex]
self.callback(actualValue)
}
}
}

iOS how to make slider stop at discrete points

To make the slider "stick" at specific points, your viewcontroller should, in the valueChanged method linked to from the slider, determine the appropriate rounded from the slider's value and then use setValue: animated: to move the slider to the appropriate place. So, if your slider goes from 0 to 2, and the user changes it to 0.75, you assume this should be 1 and set the slider value to that.

Add ticker to snapping UISlider

There are various issues you need to deal width.

To begin with, take a look at these three "default" UISlider controls. The thumb is offset vertically so we can see the track, and the dashed red outline is the slider frame:

Sample Image

  • The top one is at min value (as far left as it goes)
  • the middle one is at 50%
  • and the bottom one is at max value (as far right as it goes)

As we can see, the horizontal center of the thumb is NOT at the ends (the bounds) of the track rect.

If we want the tick marks to line up with the horizontal center of the thumb, we'll need to calculate the x-positions based on the origin and width of the thumb-centers at minimum and maximum values:

Sample Image

The next issue is that you may not want the track rect / images to extend to the left/right of the tick marks.

In that case, we need to clear the built-in track images and draw our own:

Sample Image

Here is some example code that you could try working with:

protocol TickerSliderDelegate: NSObject {
func sliderChanged(_ newValue: Int, sender: Any)
}
extension TickerSliderDelegate {
// make this delegate func optional
func sliderChanged(_ newValue: Int, sender: Any) {}
}

class TickerSlider: UISlider {

var delegate: TickerSliderDelegate?

var stepCount = 12

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// clear min and max track images
// because we'll be drawing our own
setMinimumTrackImage(UIImage(), for: [])
setMaximumTrackImage(UIImage(), for: [])
}

override func draw(_ rect: CGRect) {
super.draw(rect)

// get the track rect
let trackR: CGRect = self.trackRect(forBounds: bounds)

// get the thumb rect at min and max values
let minThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: minimumValue)
let maxThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: maximumValue)

// usable width is center of thumb to center of thumb at min and max values
let usableWidth: CGFloat = maxThumbR.midX - minThumbR.midX

// Tick Height (or use desired explicit height)
let tickHeight: CGFloat = bounds.height

// "gap" between tick marks
let stepWidth: CGFloat = usableWidth / CGFloat(stepCount)

// a reusable path
var pth: UIBezierPath!

// a reusable point
var pt: CGPoint!

// new path
pth = UIBezierPath()

// left end of our track rect
pt = CGPoint(x: minThumbR.midX, y: bounds.height * 0.5)

// top of vertical tick lines
pt.y = (bounds.height - tickHeight) * 0.5

// we have to draw stepCount + 1 lines
// so use
// 0...stepCount
// not
// 0..<stepCount
for _ in 0...stepCount {
pth.move(to: pt)
pth.addLine(to: CGPoint(x: pt.x, y: pt.y + tickHeight))
pt.x += stepWidth
}
UIColor.lightGray.setStroke()
pth.stroke()

// new path
pth = UIBezierPath()

// left end of our track lines
pt = CGPoint(x: minThumbR.midX, y: bounds.height * 0.5)

// move to left end
pth.move(to: pt)

// draw the "right-side" of the track first
// it will be the full width of "our track"
pth.addLine(to: CGPoint(x: pt.x + usableWidth, y: pt.y))
pth.lineWidth = 3
UIColor.lightGray.setStroke()
pth.stroke()

// new path
pth = UIBezierPath()

// move to left end
pth.move(to: pt)

// draw the "left-side" of the track on top of the "right-side"
// at percentage width
let rng: Float = maximumValue - minimumValue
let val: Float = value - minimumValue
let pct: Float = val / rng
pth.addLine(to: CGPoint(x: pt.x + (usableWidth * CGFloat(pct)), y: pt.y))
pth.lineWidth = 3
UIColor.systemBlue.setStroke()
pth.stroke()

}

override func setValue(_ value: Float, animated: Bool) {
// don't allow value outside range of min and max values
let newVal: Float = min(max(minimumValue, value), maximumValue)
super.setValue(newVal, animated: animated)

// we need to trigger draw() when the value changes
setNeedsDisplay()
let steps: Float = Float(stepCount)
let rng: Float = maximumValue - minimumValue
// get the percentage along the track
let pct: Float = newVal / rng
// use that pct to get the rounded step position
let pos: Float = round(steps * pct)
// tell the delegate which Tick the thumb snapped to
delegate?.sliderChanged(Int(pos), sender: self)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)

let steps: Float = Float(stepCount)
let rng: Float = maximumValue - minimumValue
// get the percentage along the track
let pct: Float = value / rng
// use that pct to get the rounded step position
let pos: Float = round(steps * pct)
// use that pos to calculate the new percentage
let newPct: Float = (pos / steps)
let newVal: Float = minimumValue + (rng * newPct)
self.value = newVal
}
override var bounds: CGRect {
willSet {
// we need to trigger draw() when the bounds changes
setNeedsDisplay()
}
}

}

Note: this is just one approach to this task (and is Example Code Only). Searching turns up many other approaches / examples / etc that you may want to take a look at.


Edit

This is a very slightly modified version to get a more "point accurate" tick-mark / thumb alignment:

class TickerSlider: UISlider {

var delegate: TickerSliderDelegate?

var stepCount = 12

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {

// clear min and max track images
// because we'll be drawing our own
setMinimumTrackImage(UIImage(), for: [])
setMaximumTrackImage(UIImage(), for: [])

// if we're using a custom thumb image
if let img = UIImage(named: "CustomThumbA") {
self.setThumbImage(img, for: [])
}

}

override func draw(_ rect: CGRect) {
super.draw(rect)

// get the track rect
let trackR: CGRect = self.trackRect(forBounds: bounds)

// get the thumb rect at min and max values
let minThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: minimumValue)
let maxThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: maximumValue)

// usable width is center of thumb to center of thumb at min and max values
let usableWidth: CGFloat = maxThumbR.midX - minThumbR.midX

// Tick Height (or use desired explicit height)
let tickHeight: CGFloat = bounds.height

// a reusable path
var pth: UIBezierPath!

// a reusable point
var pt: CGPoint!

// new path
pth = UIBezierPath()

pt = .zero

// top of vertical tick lines
pt.y = (bounds.height - tickHeight) * 0.5

// we have to draw stepCount + 1 lines
// so use
// 0...stepCount
// not
// 0..<stepCount
for i in 0...stepCount {
// get center of Thumb at each "step"
let aThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: Float(i) / Float(stepCount))
pt.x = aThumbR.midX
pth.move(to: pt)
pth.addLine(to: CGPoint(x: pt.x, y: pt.y + tickHeight))
}
UIColor.lightGray.setStroke()
pth.stroke()

// new path
pth = UIBezierPath()

// left end of our track lines
pt = CGPoint(x: minThumbR.midX, y: bounds.height * 0.5)

// move to left end
pth.move(to: pt)

// draw the "right-side" of the track first
// it will be the full width of "our track"
pth.addLine(to: CGPoint(x: pt.x + usableWidth, y: pt.y))
pth.lineWidth = 3
UIColor.lightGray.setStroke()
pth.stroke()

// new path
pth = UIBezierPath()

// move to left end
pth.move(to: pt)

// draw the "left-side" of the track on top of the "right-side"
// at percentage width
let rng: Float = maximumValue - minimumValue
let val: Float = value - minimumValue
let pct: Float = val / rng
pth.addLine(to: CGPoint(x: pt.x + (usableWidth * CGFloat(pct)), y: pt.y))
pth.lineWidth = 3
UIColor.systemBlue.setStroke()
pth.stroke()

}

override func setValue(_ value: Float, animated: Bool) {
// don't allow value outside range of min and max values
let newVal: Float = min(max(minimumValue, value), maximumValue)
super.setValue(newVal, animated: animated)

// we need to trigger draw() when the value changes
setNeedsDisplay()
let steps: Float = Float(stepCount)
let rng: Float = maximumValue - minimumValue
// get the percentage along the track
let pct: Float = newVal / rng
// use that pct to get the rounded step position
let pos: Float = round(steps * pct)
// tell the delegate which Tick the thumb snapped to
delegate?.sliderChanged(Int(pos), sender: self)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)

let steps: Float = Float(stepCount)
let rng: Float = maximumValue - minimumValue
// get the percentage along the track
let pct: Float = value / rng
// use that pct to get the rounded step position
let pos: Float = round(steps * pct)
// use that pos to calculate the new percentage
let newPct: Float = (pos / steps)
let newVal: Float = minimumValue + (rng * newPct)
self.value = newVal
}
override var bounds: CGRect {
willSet {
// we need to trigger draw() when the bounds changes
setNeedsDisplay()
}
}

}

The primary difference is that, in our loop that draws the tick-mark lines, instead of calculating the "gap" values, we get the "thumb center" for each step:

    for i in 0...stepCount {
// get center of Thumb at each "step"
let aThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: Float(i) / Float(stepCount))
pt.x = aThumbR.midX
pth.move(to: pt)
pth.addLine(to: CGPoint(x: pt.x, y: pt.y + tickHeight))
}

Here's an image using a custom thumb image, at steps 0, 1 & 2:

Sample Image

and the @2x (62x62) and @3x (93x93) images I used for the thumb:

Sample Image

Sample Image

How to animate slider to marker in double side slider

I've done some changes in code as per your requirement. Please check this.

Add this method when you alloc init this control.

[self.rangeSlider addTarget:self action:@selector(rangeSliderValueChanged:) forControlEvents:UIControlEventValueChanged];//This is already added.
[self.rangeSlider addTarget:self action:@selector(dragEnded:) forControlEvents:UIControlEventTouchUpOutside];//add this

Implement this method -- this method will call when you drag end.

-(void)dragEnded:(id)sender
{
NSLog(@"called");
//Set your Conditions this is for testing.
if (self.rangeSlider.leftValue<15.0)
{
self.rangeSlider.leftValue = 10.0;
}
}

In REDRangeSlider Class Check this two methods which handle Pan Gesture states, so when you drag end then method will call this gesture state. at that time call this action which you integrate in your main class.

- (void)leftHandlePanEngadged:(UIGestureRecognizer *)gesture 
{
else if (panGesture.state == UIGestureRecognizerStateCancelled ||
panGesture.state == UIGestureRecognizerStateEnded ||
panGesture.state == UIGestureRecognizerStateCancelled) {
self.leftHandle.highlighted = NO;
self.leftValue = [self roundValueToStepValue:self.leftValue];
//Change below line For Left .
[self sendActionsForControlEvents:UIControlEventTouchUpOutside];
}
}
- (void)rightHandlePanEngadged:(UIGestureRecognizer *)gesture
{
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gesture;

//Other code ........
else if (panGesture.state == UIGestureRecognizerStateCancelled ||
panGesture.state == UIGestureRecognizerStateEnded ||
panGesture.state == UIGestureRecognizerStateCancelled) {
self.rightHandle.highlighted = NO;
self.rightValue = [self roundValueToStepValue:self.rightValue];
//Change below line For right .
[self sendActionsForControlEvents:UIControlEventTouchUpOutside];
}
}

IOS UISlider non continuos approximate value

Here's some code for you, base on a stackoverflow post somewhere...

- (IBAction)terrainValueChanged:(id)sender {

float newStep = roundf((terrainSlider.value) / self.stepValue);
self.terrainSlider.value = newStep * self.stepValue;
int intValue = self.terrainSlider.value;
terrainRating = intValue;

}

Initialize with this:

    stepValue = 1;
self.terrainStep = (self.terrainSlider.value) / self.terrainStep;


Related Topics



Leave a reply



Submit