2
votes

I have a CAShapelayer circle that I animate as a progress circle with CABasicAnimation from 0 back to 360 degrees alongside a countdown timer in statemachine with Reset, Play, Pause, and Complete states.

Right now, I can play or pause and resume until the timer finishes and the animation completes and model goes back to its original values, then pick another countdown timer value to start with animation.

However, if I play and then reset at anytime before animation is complete, it cancels but when I go to play again, the animation no longer works. I am noticing that for some reason after I reset before animation is complete, my strokeEnd doesn't start at 0.0 again and instead gets a seemingly arbitrary decimal point value. I think this is the root cause of my issue, but I don't know why the strokeEnd value are these random numbers. Here's a screenshot of the strokeEnd values - https://imgur.com/a/YXmNkaK

Here's what I have so far:

//draws CAShapelayer 
func drawCircle() {}

//CAShapeLayer animation
func progressCircleAnimation(transitionDuration: TimeInterval, speed: Double, strokeEnd: Double) {
    let fillLineAnimation = CABasicAnimation(keyPath: "strokeEnd")
    fillLineAnimation.duration = transitionDuration
    fillLineAnimation.fromValue = 0
    fillLineAnimation.toValue = 1.0
    fillLineAnimation.speed = Float(speed)
    circleWithProgressBorderLayer.strokeEnd = CGFloat(strokeEnd)
    circleWithProgressBorderLayer.add(fillLineAnimation, forKey: "lineFill")
}

//Mark: Pause animation 
func pauseLayer(layer : CALayer) {
    let pausedTime : CFTimeInterval = circleWithProgressBorderLayer.convertTime(CACurrentMediaTime(), from: nil)
    circleWithProgressBorderLayer.speed = 0.0
    circleWithProgressBorderLayer.timeOffset = pausedTime
}

//Mark: Resume CABasic Animation on CAShaperLayer at pause offset
func resumeLayer(layer : CALayer) {
    let pausedTime = circleWithProgressBorderLayer.timeOffset
    circleWithProgressBorderLayer.speed = 1.0;
    circleWithProgressBorderLayer.timeOffset = 0.0;
    circleWithProgressBorderLayer.beginTime = 0.0;
    let timeSincePause = circleWithProgressBorderLayer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
    circleWithProgressBorderLayer.beginTime = timeSincePause;
}

//Mark: Tried to removeAnimation
func resetLayer(layer : CALayer) {
    layer.removeAnimation(forKey: "lineFill")
    circleWithProgressBorderLayer.strokeEnd = 0.0
}

Here's how I have it set up in my statemachine, in case the error has to do with this:

@objc func timerIsReset() {
    resetTimer()
    let currentRow = timePickerView.selectedRow(inComponent: 0)
    self.counter = self.Times[currentRow].amount
}

//Mark: action for RunningTimerState
@objc func timerIsStarted() {
    runTimer()
}

//Mark: action for PausedTimerState
@objc func timerIsPaused() {

    pauseTimer()
}

//Mark: action for TimerTimeRunOutState
func timerTimeRunOut() {

    resetTimer()
}

func runTimer() {
    if isPaused == true {
        finishTime = Date().addingTimeInterval(-remainingTime)
        timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)

        resumeLayer(layer: circleWithProgressBorderLayer)

    } else if isPaused == false {
        finishTime = Date().addingTimeInterval(counter)
        timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)

        progressCircleAnimation(transitionDuration: counter, speed: 1.0, strokeEnd: self.completionPercentage)
    }
}

//Mark: Pause Timer and pause CABasic Animation
func pauseTimer() {
    timer.invalidate()
    isPaused = true
    remainingTime = -finishTime.timeIntervalSinceNow
    completionPercentage = (counter + remainingTime) / counter

    pauseLayer(layer: circleWithProgressBorderLayer)
}

func resetTimer() {
    timer.invalidate()
    isPaused = false
    resetLayer(layer: circleWithProgressBorderLayer)
}
1
UIViewPropertyAnimator is for animating view properties. CAShapeLayer is not a view and has no view properties. So using CABasicAnimation is correct. To animate a layer, use layer animation.matt
@matt thank you. Is there a way to pause and resume CABasicAnimation?agastache
Yes, sure. To pause, just set the layer's speed to 0. That's all the UIViewPropertyAnimator is really doing under the hood. It isn't magic.matt

1 Answers

0
votes

Lets say your animation looks something like this...

func runTimerMaskAnimation(duration: CFTimeInterval, fromValue : Double){

       ...
             let path = UIBezierPath(roundedRect: circleBounds, cornerRadius: 
             circleBounds.size.width * 0.5)
             maskLayer?.path = path.reversing().cgPath
             maskLayer?.strokeEnd = 0
    
             parentCALayer.mask = maskLayer
    
             let animation = CABasicAnimation(keyPath: "strokeEnd")
             animation.duration = duration
             animation.fromValue = fromValue
             animation.toValue = 0.0
             maskLayer?.add(animation!, forKey: "strokeEnd")
}

If I wanted to restart my timer to the original position before the animation is complete I would remove the animation, remove the maskLayer and then run the animation again.

 maskLayer?.removeAnimation(forKey: "strokeEnd")
 maskLayer?.removeFromSuperlayer()
 runTimerMaskAnimation(duration: 15, fromValue : 1)