Reputation: 3
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
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
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