I made a custom slider by subclassing UISlider, and I'm trying to add scrubbing functionality to it, but for some reason the scrubbing is barely even noticeable at 0.1? In my code, I tried multiplying change in x distance by the scrubbing value, but it doesn't seem to work. Also, when I manually set the scrubbing speed to a lower value such as 0.01, it does go slower but it looks really laggy and weird?? What am I doing wrong?
Any help or advice would be greatly appreciated!
Subclass of UISlider:
class SizeSliderView: UISlider {
private var previousLocation: CGPoint?
private var currentLocation: CGPoint?
private var translation: CGFloat = 0
private var scrubbingSpeed: CGFloat = 1
private var defaultDiameter: Float
init(startValue: Float = 0, defaultDiameter: Float = 500) {
self.defaultDiameter = defaultDiameter
super.init(frame: .zero)
value = clamp(value: startValue, min: minimumValue, max: maximumValue)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
clear()
createThumbImageView()
addTarget(self, action: #selector(valueChanged(_:)), for: .valueChanged)
}
// Clear elements
private func clear() {
tintColor = .clear
maximumTrackTintColor = .clear
backgroundColor = .clear
thumbTintColor = .clear
}
// Call when value is changed
@objc private func valueChanged(_ sender: SizeSliderView) {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.commit()
createThumbImageView()
}
// Create thumb image with thumb diameter dependent on thumb value
private func createThumbImageView() {
let thumbDiameter = CGFloat(defaultDiameter * value)
let thumbImage = UIColor.red.circle(CGSize(width: thumbDiameter, height: thumbDiameter))
setThumbImage(thumbImage, for: .normal)
setThumbImage(thumbImage, for: .highlighted)
setThumbImage(thumbImage, for: .application)
setThumbImage(thumbImage, for: .disabled)
setThumbImage(thumbImage, for: .focused)
setThumbImage(thumbImage, for: .reserved)
setThumbImage(thumbImage, for: .selected)
}
// Return true so touches are tracked
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
// Ensure that start location is on thumb
let thumbDiameter = CGFloat(defaultDiameter * value)
if location.x < bounds.width / 2 - thumbDiameter / 2 || location.x > bounds.width / 2 + thumbDiameter / 2 || location.y < 0 || location.y > thumbDiameter {
return false
}
previousLocation = location
super.beginTracking(touch, with: event)
return true
}
// Track based on moving slider
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
guard isTracking else { return false }
guard let previousLocation = previousLocation else { return false }
// Reference
// location: location of touch relative to device
// delta location: change in touch location WITH scrubbing
// adjusted location: location of touch to slider bounds (WITH scrubbing)
// translation: location of slider relative to device
let location = touch.location(in: self)
currentLocation = location
scrubbingSpeed = getScrubbingSpeed(for: location.y - 50)
let deltaLocation = (location.x - previousLocation.x) * scrubbingSpeed
var adjustedLocation = deltaLocation + previousLocation.x - translation
if adjustedLocation < 0 {
translation += adjustedLocation
adjustedLocation = deltaLocation + previousLocation.x - translation
} else if adjustedLocation > bounds.width {
translation += adjustedLocation - bounds.width
adjustedLocation = deltaLocation + previousLocation.x - translation
}
self.previousLocation = CGPoint(x: deltaLocation + previousLocation.x, y: location.y)
let newValue = Float(adjustedLocation / bounds.width) * (maximumValue - minimumValue) + minimumValue
setValue(newValue, animated: false)
sendActions(for: .valueChanged)
return true
}
// Reset start and current location
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.currentLocation = nil
self.translation = 0
super.touchesEnded(touches, with: event)
}
// Thumb location follows current location and resets in middle
override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
let thumbDiameter = CGFloat(defaultDiameter * value)
let origin = CGPoint(x: (currentLocation?.x ?? bounds.width / 2) - thumbDiameter / 2, y: (currentLocation?.y ?? thumbDiameter / 2) - thumbDiameter / 2)
return CGRect(origin: origin, size: CGSize(width: thumbDiameter, height: thumbDiameter))
}
private func getScrubbingSpeed(for value: CGFloat) -> CGFloat {
switch value {
case 0:
return 1
case 0...50:
return 0.5
case 50...100:
return 0.25
case 100...:
return 0.1
default:
return 1
}
}
private func clamp(value: Float, min: Float, max: Float) -> Float {
if value < min {
return min
} else if value > max {
return max
} else {
return value
}
}
}
UIView representative:
struct SizeSlider: UIViewRepresentable {
private var startValue: Float
private var defaultDiameter: Float
init(startValue: Float, defaultDiameter: Float) {
self.startValue = startValue
self.defaultDiameter = defaultDiameter
}
func makeUIView(context: Context) -> SizeSliderView {
let view = SizeSliderView(startValue: startValue, defaultDiameter: defaultDiameter)
view.minimumValue = 0.1
view.maximumValue = 1
return view
}
func updateUIView(_ uiView: SizeSliderView, context: Context) {}
}
Content view:
struct ContentView: View {
var body: some View {
SizeSlider(startValue: 0.20, defaultDiameter: 100)
.frame(width: 400)
}
}