Reputation: 1
I am trying to implement a custom UISlider on an iOS device.
First, the ThumbSlider is positioned on both ends, and the desired behavior is as follows: When the ThumbSlider moves left or right, the TopView between them should expand or contract simultaneously.
The above view works correctly when the TopView is implemented as a UIView.
Similarly, it also works well when using Core Graphics.
However, when the TopView is implemented using Core Animation, there is a noticeable delay between the ThumbSlider and the animation.
This behavior is completely different from what I expected. While I understand that UIView is likely optimized for this kind of operation, I cannot understand why Core Animation introduces a delay compared to Core Graphics.
Here are my final questions:
final class EditSliderBar: UIControl {
enum Constants {
static let sliderWidth: CGFloat = 16
}
// MARK: - UI Components
private let lowerThumbSlider = EditSlider(tintColor: .systemYellow)
private let upperThumbSlider = EditSlider(tintColor: .systemYellow)
private let topView = UIView()
private let topLayer = CALayer()
weak var currentHighlightedThumbSlider: EditSlider?
private var previousLocation: CGPoint = .zero
private var minimumValue: Double = 0
private var maximumValue: Double = 100
...
private(set) var lowerValue: Double = 0.0 {
didSet {
updateSliderFrame(lowerThumbSlider)
}
}
private(set) var upperValue: Double = 100 {
didSet {
updateSliderFrame(upperThumbSlider)
}
}
...
// MARK: - UIControl
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
defer { previousLocation = location }
if lowerSlider.contain(point: location) {
lowerSlider.isHightlighted = true
currentHighlightedSlider = lowerSlider
} else if upperSlider.contain(point: location) {
upperSlider.isHightlighted = true
currentHighlightedSlider = upperSlider
}
return lowerSlider.isHightlighted || upperSlider.isHightlighted
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
defer { previousLocation = location }
guard let slider = currentHighlightedThumbSlider else { return false }
let sliderDeltaValue = deltaValue(from: previousLocation, to: location)
if slider === lowerThumbSlider {
self.lowerValue = updatedLowerValue(moved: sliderDeltaValue)
} else if slider === upperThumbSlider {
self.upperValue = updatedUpperValue(moved: sliderDeltaValue)
}
sendActions(for: .valueChanged)
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
...
}
// MARK: - LayoutSubviews
override func layoutSubviews() {
super.layoutSubviews()
updateSliderFrames()
}
// MARK: - Draw
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let maskedRect = CGRect(
x: lowerSlider.center.x,
y: 0,
width: upperThumbSlider.center.x - lowerThumbSlider.center.x,
height: 5
)
context.setFillColor(UIColor.systemYellow.cgColor)
context.fill(maskedRect)
}
}
// MARK: - Private Methotds
private extension EditSliderBar {
func updateSliderFrames() {
updateSliderFrame(lowerThumbSlider)
updateSliderFrame(upperThumbSlider)
}
func updateSliderFrame(_ slider: EditSlider) {
let width = Constants.sliderWidth
let leading = slider === lowerThumbSlider ? leading(of: lowerValue) : leading(of: upperValue)
slider.frame = CGRect(
x: leading,
y: 0,
width: width,
height: bounds.height
)
setNeedsDisplay()
/// Core Animation
updateTopLayer()
/// Core Graphics
setNeedsDisplay()
/// UIView
updateTopView()
}
// Core Animation
func updateTopLayer() {
topLayer.frame = CGRect(
x: lowerThumbSlider.center.x,
y: 0,
width: upperThumbSlider.frame.maxX - lowerThumbSlider.center.x,
height: 5
)
topLayer.backgroundColor = UIColor.systemYellow.cgColor
}
// UIView
func updateTopView() {
topView.frame = CGRect(
x: lowerSlider.center.x,
y: 0,
width: upperThumbSlider.frame.maxX - lowerThumbSlider.center.x,
height: 5
)
topView.backgroundColor = UIColor.systemYellow
}
func updatedLowerValue(moved delta: Double) -> Double {
return (lowerValue + delta).bound(lower: minimumValue, upper: upperValue - gapBetweenSliders)
}
func updatedUpperValue(moved delta: Double) -> Double {
return (upperValue + delta).bound(lower: lowerValue + gapBetweenSliders, upper: maximumValue)
}
func deltaValue(from previous: CGPoint, to current: CGPoint) -> Double {
let deltaLocation = Double(current.x - previous.x)
return (maximumValue - minimumValue) * deltaLocation / Double(totalLength)
}
func leading(of value: Double) -> Double {
return totalLength * value / maximumValue
}
}
Upvotes: 0
Views: 52
Reputation: 131398
What you are seeing is not a "delay". It's an extra animation that Core Animation is adding.
Some CALayer
properties are "implicitly animated". That means that when you change a property, the system creates an animation for you.
You probably need to disable implicit animations before changing those properties.
That code might looks like this:
CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// Change CALayer properties that you don’t want to animate here.
CATransaction.commit()
So you'd rewrite your updateTopLayer()
function like this:
func updateTopLayer() {
CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
topLayer.frame = CGRect(
x: lowerThumbSlider.center.x,
y: 0,
width: upperThumbSlider.frame.maxX - lowerThumbSlider.center.x,
height: 5
)
topLayer.backgroundColor = UIColor.systemYellow.cgColor
CATransaction.commit()
}
Upvotes: 0