Georgina Brough
Georgina Brough

Reputation: 3

Why is my animation quicker than the timer?

I have an animation that I want to coincide with the timer but right now it ends with 6seconds left of the timer. How do I get the animation to match? Also, how would i go about repeating the animation for the countdown of the iteration, i?

The code has the animation, in a circle, and then preset timer of 30s (which will eventually be a slider input). I will also eventually want to include a pause, and stop button for the timer which will need to coincide with the animation

 import UIKit

 var timer = Timer()
 var time = 30
 var i = 5

 class ViewController: UIViewController {

  @IBOutlet weak var displayTime: UILabel!


 let shape = CAShapeLayer()
 private let label: UILabel = {
    let label = UILabel()
    label.text = String(i)
    // change label to update i
    label.font = .systemFont(ofSize: 36, weight: .light)
    return label
}()

 func countdown() {
    displayTime.text = String(time)
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: 
 #selector(doCountdown), userInfo: nil, repeats: true)
 }

override func viewDidLoad() {
    super.viewDidLoad()
    label.sizeToFit()
    view.addSubview(label)
    label.center = view.center
    
    let circlePath = UIBezierPath(arcCenter: view.center, radius: 150, startAngle: - 
  (.pi / 2), endAngle: .pi * 2, clockwise: true)
    
    let trackShape = CAShapeLayer()
    trackShape.path = circlePath.cgPath
    trackShape.fillColor = UIColor.clear.cgColor
    trackShape.lineWidth = 15
    trackShape.strokeColor = UIColor.lightGray.cgColor
    view.layer.addSublayer(trackShape)
    
    
    shape.path = circlePath.cgPath
    shape.lineWidth = 15
    shape.strokeColor = UIColor.blue.cgColor
    shape.fillColor = UIColor.clear.cgColor
    shape.strokeEnd = 0
    // cg = core graphics
    
    view.layer.addSublayer(shape)
    
    let button = UIButton(frame: CGRect(x: 20, y: view.frame.size.height-70, width: 
  view.frame.size.width-40, height: 50))
    view.addSubview(button)
    button.setTitle("Animate", for: .normal)
    button.backgroundColor = .systemGreen
    button.addTarget(self, action:#selector(didTapButton), for: .touchUpInside)
  }

   @objc func didTapButton() {
    countdown()
    // Animate
    let animation = CABasicAnimation(keyPath: "strokeEnd")
    animation.toValue = 1
    animation.duration = Double(time)
    // duration of animation
    animation.isRemovedOnCompletion = false
    animation.fillMode = .forwards
    shape.add(animation, forKey: "animation")
   }

  @objc func doCountdown() {
    time = time - 1
    displayTime.text = String(time)
    if time == 0 {
        i = i - 1
        time = 30
    }
    if i == 0 {
        label.text = "0"
         timer.invalidate()
    }
   }

   }

Upvotes: 0

Views: 49

Answers (2)

Sulthan
Sulthan

Reputation: 130132

Your implementation does not work because you are using a naive implementation of countdown.

A timer is not guaranteed to fire exactly after the given amount of time. It won't fire exactly after one second. The accuracy of Timer is 50-100 milliseconds. Therefore the total possible error can add up to 30*100 milliseconds, that is 3 entire seconds.

Instead, you have to use a Timer that will update your UI more often:

timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: 
 #selector(doCountdown), userInfo: nil, repeats: true)

And that also means you have to calculate your time differently. First of all, store the expected time of animation end:

// declare instance properties
private var animationEnd = Date()

private var timer: Timer? {
   didSet {
     // invalidate when nil is assigned
     oldValue?.invalidate()
   }
}
func startCountdown() {
  // store the start time - 30 seconds in the future
  animationEnd = Date().addingTimeInterval(TimerInterval(time))

  // start the timer
  timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
     guard let self = self else { return }

     let remainingTime = max(0, self.animationEnd.timeIntervalSinceNow)
     if remainingTime == 0 {
       // invalidate the timer
       self.timer = nil      
     }

     // convert time to seconds
     let remaininingSeconds = Int(remainingTime) ?? 0
     self.displayTime.text = "\(remaininingSeconds)"
  }
}
//

That's all.

If you want to pause & resume the timer, the process is the same. Invalidate the timer, store the current time (e.g. timePaused = Date) and when resumed, just add the difference between current time and timePaused to animationEnd and restart the timer.

Also, please, don't put variables on file level. Always put them to the scope of classes. Otherwise you will soon have problems.

Upvotes: 2

Tarun Tyagi
Tarun Tyagi

Reputation: 10112

I think these two variables are the source of your problem -

 var time = 30
 var i = 5

Can you try deleting the i variable and use this updated implementation -

    @objc func doCountdown() {
        time -= 1
        displayTime.text = String(time)
        if time == 0 {
            timer.invalidate()
        }
    }

Upvotes: 0

Related Questions