Custom UISlider visualisation error (thumb position, track edge...) - ios

this is my first time asking question here, if there is any part unclear in my question, please let me know, thank you.
So I am new to iOS, recently for a project I created a custom slider which looks like a iOS-style switch. Below is my code:
#IBDesignable
class CustomSlider: UISlider {
#IBInspectable var trackWidth: CGFloat = 70
#IBInspectable var trackHeight: CGFloat = 30
#IBInspectable var thumbHeight: CGFloat = 30
private var thumbTouchSize = CGSize(width: 70, height: 30)
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: bounds.origin, size: CGSize(width: bounds.width, height: trackHeight))
}
override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
var thumb = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
thumb.size.height = thumbHeight
return thumb.offsetBy(dx: 0, dy: 0)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let increasedBounds = bounds.insetBy(dx: -thumbTouchSize.width, dy: -thumbTouchSize.height)
let containsPoint = increasedBounds.contains(point)
return containsPoint
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let percentage = CGFloat((value - minimumValue) / (maximumValue - minimumValue))
let thumbSizeHeight = thumbRect(forBounds: bounds, trackRect:trackRect(forBounds: bounds), value:0).size.height
let thumbPosition = thumbSizeHeight + (percentage * (bounds.size.width - (2 * thumbSizeHeight)))
let touchLocation = touch.location(in: self)
return touchLocation.x <= (thumbPosition + thumbTouchSize.width) && touchLocation.x >= (thumbPosition - thumbTouchSize.width)
}
}
This is how it looks :
custom UISlider initial state
when thumb moves towards right end
custom UISlider max. value
As you can see,
(1) I set same height for the track and the thumb, however the thumb is not in the vertical center of the track, which does not look good.
(2) when I pull the slider to the right end, the edge becomes flat which should not happen (edges should always remain round like the initial state).
I tried all the suggested solutions, none solved my problem. Please help, many thanks in advance.

Related

Add ticker to snapping UISlider

I am having some issues creating a custom UISlider that snaps at certain values, and also have tick marks at those values. I have the snapping part working, but the tick marks is an issue. The Slider snaps in 12 positions, every 5 from 15-70. I've tried a variety of approaches, including adding 12 subviews to a stack view and placing that on the slider, and also calculating the step width between each. Both approaches placed the ticks not where the thumb snaps to. Does anyone know how to do this correctly?
Here's the second approach:
let stepCount = 12
print("THE WIDTH: \(self.bounds.width)")
guard let imageWidth = self.currentThumbImage?.size.width else {return}
guard let imageWidth = self.currentThumbImage?.size.width else {return}
let stepWidth = bounds.width / CGFloat(stepCount)
for i in 0..<stepCount {
let view = UIView(frame: CGRect(x: stepWidth / 2 + stepWidth * CGFloat(i) - imageWidth / 2, y: 0, width: 1, height: 29))
view.backgroundColor = .lightGray
self.insertSubview(view, at: 0)
}
Here is the snapping code:
#objc func valueChanged(_ sender: UISlider){
let step: Float = 5
let roundedValue = round(sender.value / step) * step
self.value = roundedValue
delegate?.sliderChanged(Int(roundedValue), sender)
}
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:
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:
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:
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:
and the #2x (62x62) and #3x (93x93) images I used for the thumb:

Increasing slider width : swift

I'm using UISlider in swift to show slider on my app. Is there a way by which we can increase the width of the slider ?
I have tried to transform the slider using CGAffineTransform. But this also increases the thumb size.
testslider.transform = CGAffineTransform(rotationAngle: CGFloat(-Double.pi / 2))
I just need to increase width of the slider line but not the thumb size. Is there any way we can do that? Please suggest.
Try the following code
open class CustomSlider : UISlider {
#IBInspectable open var trackWidth:CGFloat = 2 {
didSet {setNeedsDisplay()}
}
override open func trackRect(forBounds bounds: CGRect) -> CGRect {
let defaultBounds = super.trackRect(forBounds: bounds)
return CGRect(
x: defaultBounds.origin.x,
y: defaultBounds.origin.y + defaultBounds.size.height/2 - trackWidth/2,
width: defaultBounds.size.width,
height: trackWidth
)
}
}

Increase UISlider height in Swift 3 without using transform [duplicate]

I've been searching for a way to make the UISlider progress bar taller, like increasing the height of the slider but couldn't find anything. I don't want to use a custom image or anything, just make it taller, so the UISlider doesn't look so thin. Is there an easy way to do this that I'm missing?
The accepted answer will undesirably change the slider's width in some cases, like if you're using a minimumValueImage and maximumValueImage. If you only want to change the height and leave everything else alone, then use this code:
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var newBounds = super.trackRect(forBounds: bounds)
newBounds.size.height = 12
return newBounds
}
Here's my recent swifty implementation, building on CularBytes's ...
open class CustomSlider : UISlider {
#IBInspectable open var trackWidth:CGFloat = 2 {
didSet {setNeedsDisplay()}
}
override open func trackRect(forBounds bounds: CGRect) -> CGRect {
let defaultBounds = super.trackRect(forBounds: bounds)
return CGRect(
x: defaultBounds.origin.x,
y: defaultBounds.origin.y + defaultBounds.size.height/2 - trackWidth/2,
width: defaultBounds.size.width,
height: trackWidth
)
}
}
Use this on a UISlider in a storyboard by setting its custom class
The IBInspectable allows you to set the height from the storyboard
For those that would like to see some working code for changing the track size.
class CustomUISlider : UISlider {
override func trackRect(forBounds bounds: CGRect) -> CGRect {
//keeps original origin and width, changes height, you get the idea
let customBounds = CGRect(origin: bounds.origin, size: CGSize(width: bounds.size.width, height: 5.0))
super.trackRect(forBounds: customBounds)
return customBounds
}
//while we are here, why not change the image here as well? (bonus material)
override func awakeFromNib() {
self.setThumbImage(UIImage(named: "customThumb"), for: .normal)
super.awakeFromNib()
}
}
Only thing left is changing the class inside the storyboard:
You can keep using your seekbar action and outlet to the object type UISlider, unless you want to add some more custom stuff to your slider.
I found what I was looking for. The following method just needs to be edited in a subclass.
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var customBounds = super.trackRect(forBounds: bounds)
customBounds.size.height = ...
return customBounds
}
You could play with this, see what happens:
slider.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.0, 2.0);

How to customise UISlider height

I have project in which have to customise UISlider element.
I wondering if someone knows how to change, or is it possible to change height of UISlide bar line.
I tried something like this but don't work:
let customBounds = CGRect(origin: bounds.origin,
size: CGSize(width: bounds.size.width, height: 15.0))
feedbackSlider.trackRectForBounds(customBounds)
Thanks
i hope that you want edit it in storyboard, and only the line size, use it in your custom UISlider
class CustomSlide: UISlider {
#IBInspectable var trackHeight: CGFloat = 2
override func trackRectForBounds(bounds: CGRect) -> CGRect {
//set your bounds here
return CGRect(origin: bounds.origin, size: CGSizeMake(bounds.width, trackHeight))
}
}
Overriding trackRect is a way to go, however if you're using additional UISlider's views like minimumValueImage, maximumValueImage you would also need to take their bound into account, otherwise they will overlap with slider’s track. As a shortcut you can simply use super's func:
Fixed version.
Swift 3+
#IBDesignable
class CustomSlider: UISlider {
/// custom slider track height
#IBInspectable var trackHeight: CGFloat = 3
override func trackRect(forBounds bounds: CGRect) -> CGRect {
// Use properly calculated rect
var newRect = super.trackRect(forBounds: bounds)
newRect.size.height = trackHeight
return newRect
}
}
You can override a method in you custom slider
For Objective-C
- (CGRect)trackRectForBounds:(CGRect)bounds {
CGRect rect = CGRectMake(0, 0, 100, 30);//change it to any size you want
return rect;
}
For Swift
override func trackRectForBounds(bounds: CGRect) -> CGRect {
var rect:CGRect = CGRectMake(0, 0, 100, 30)
return rect
}
Swift 3
class MySlide: UISlider {
#IBInspectable var height: CGFloat = 2
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: bounds.origin, size: CGSize(width: bounds.width, height: height))
}
}
Swift 4 version without thumbImage.
class CustomSlider: UISlider {
#IBInspectable var trackHeight: CGFloat = 2
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: bounds.origin, size: CGSize(width: bounds.width, height: trackHeight))
}
}
I think the answers above have a flaw: the origin of the track rect must be offset to account for the change in height of the track, otherwise it will show up off-center. Here is the way I did it:
class CustomSlider: UISlider {
#IBInspectable var sliderTrackHeight : CGFloat = 2
override func trackRect(forBounds bounds: CGRect) -> CGRect {
let originalRect = super.trackRect(forBounds: bounds)
return CGRect(origin: CGPoint(x: originalRect.origin.x, y: originalRect.origin.y + (sliderTrackHeight / 2)), size: CGSize(width: bounds.width, height: sliderTrackHeight))
}
}
U can easily get it done by subclassing UISlider. see following code
class CustomUISlider : UISlider
{
override func trackRectForBounds(bounds: CGRect) -> CGRect {
//set your bounds here
return bounds
}
}
You should be able to subclass the UISlider and then implement:
- (CGRect)trackRectForBounds:(CGRect)bounds
Just return the new CGRect here.
If you're using autolayout you can set a height constraint on the UISlider in the storyboard. if you need to change it at runtime - create an IBOutlet for the constraint and modify its .constant value.

Make UISlider height larger?

I've been searching for a way to make the UISlider progress bar taller, like increasing the height of the slider but couldn't find anything. I don't want to use a custom image or anything, just make it taller, so the UISlider doesn't look so thin. Is there an easy way to do this that I'm missing?
The accepted answer will undesirably change the slider's width in some cases, like if you're using a minimumValueImage and maximumValueImage. If you only want to change the height and leave everything else alone, then use this code:
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var newBounds = super.trackRect(forBounds: bounds)
newBounds.size.height = 12
return newBounds
}
Here's my recent swifty implementation, building on CularBytes's ...
open class CustomSlider : UISlider {
#IBInspectable open var trackWidth:CGFloat = 2 {
didSet {setNeedsDisplay()}
}
override open func trackRect(forBounds bounds: CGRect) -> CGRect {
let defaultBounds = super.trackRect(forBounds: bounds)
return CGRect(
x: defaultBounds.origin.x,
y: defaultBounds.origin.y + defaultBounds.size.height/2 - trackWidth/2,
width: defaultBounds.size.width,
height: trackWidth
)
}
}
Use this on a UISlider in a storyboard by setting its custom class
The IBInspectable allows you to set the height from the storyboard
For those that would like to see some working code for changing the track size.
class CustomUISlider : UISlider {
override func trackRect(forBounds bounds: CGRect) -> CGRect {
//keeps original origin and width, changes height, you get the idea
let customBounds = CGRect(origin: bounds.origin, size: CGSize(width: bounds.size.width, height: 5.0))
super.trackRect(forBounds: customBounds)
return customBounds
}
//while we are here, why not change the image here as well? (bonus material)
override func awakeFromNib() {
self.setThumbImage(UIImage(named: "customThumb"), for: .normal)
super.awakeFromNib()
}
}
Only thing left is changing the class inside the storyboard:
You can keep using your seekbar action and outlet to the object type UISlider, unless you want to add some more custom stuff to your slider.
I found what I was looking for. The following method just needs to be edited in a subclass.
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var customBounds = super.trackRect(forBounds: bounds)
customBounds.size.height = ...
return customBounds
}
You could play with this, see what happens:
slider.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.0, 2.0);

Resources