정석영
정석영

Reputation: 1

Why does Core Animation have a delay compared to Core Graphics?

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.

TopView as UIView

Similarly, it also works well when using Core Graphics.

TopView as Core Graphics

However, when the TopView is implemented using Core Animation, there is a noticeable delay between the ThumbSlider and the animation.

TopView as Core 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:

  1. Why is Core Animation’s drawing slower than UIView?
  2. Why is Core Graphics (CPU-based) faster than Core Animation?
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

Answers (1)

Duncan C
Duncan C

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

Related Questions