user1759949
user1759949

Reputation: 199

How to create a circular progress indicator for a count down timer

I am trying to add a countdown to my app and I'm having problems animating this. The look I'm going for is something similar to the iPad countdown shown below, with the red bar increasing as the clock counts down. enter image description here

Initially I created an image atlas with an individual image for each half second of the countdown but this seems to take a lot of memory to run and therefore crashes the app so I now have to find an alternative.

I've been looking at colorizing here https://developer.apple.com/library/prerelease/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Sprites/Sprites.html#//apple_ref/doc/uid/TP40013043-CH9 but can't see a way of colorizing a section of the sprite, it seems that the whole sprite would be changed.

Does anyone know if colorizing would be an option here, or is there a way of reduce the memory used by an image atlas?

Thanks

Upvotes: 15

Views: 18864

Answers (3)

paky
paky

Reputation: 685

Thanks for the great answer from @Leo Dabus, and I see someone asking why the code didn't work / not able to implement on their own project. Here are some tips to keep in mind when using CABasicAnimation:

  • We should add the animation to the layer that ALREADY exists in the view, i.e. ensure calling layer.add after view.layer.addSublayer.
  • Try not to do animation in viewDidLoad, as we can't tell if the layer already exists.

So we can try something like:

override func viewDidLoad() {
    super.viewDidLoad()
    drawBgShape()
    drawTimeLeftShape()
    ...
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // move the setup of animation here
    strokeIt.fromValue = 0
    strokeIt.toValue = 1
    strokeIt.duration = timeLeft
    timeLeftShapeLayer.add(strokeIt, forKey: nil)
}

Attached some great reference where help me to make the idea clear. Hope this can help and have a nice day ;)

https://stackoverflow.com/a/13066094/11207700

https://stackoverflow.com/a/43006847/11207700

Upvotes: 1

Leo Dabus
Leo Dabus

Reputation: 236360

You can do it using CAShapeLayer and animating the stroke end as follow:

update Xcode 9 • Swift 4

define the time left

let timeLeftShapeLayer = CAShapeLayer()
let bgShapeLayer = CAShapeLayer()
var timeLeft: TimeInterval = 60
var endTime: Date?
var timeLabel =  UILabel()
var timer = Timer()
// here you create your basic animation object to animate the strokeEnd
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")

define a method to create de UIBezierPath

startAngle at -90˚ and endAngle 270

func drawBgShape() {
    bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
        100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
    bgShapeLayer.strokeColor = UIColor.white.cgColor
    bgShapeLayer.fillColor = UIColor.clear.cgColor
    bgShapeLayer.lineWidth = 15
    view.layer.addSublayer(bgShapeLayer)
}
func drawTimeLeftShape() {
    timeLeftShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
        100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
    timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
    timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
    timeLeftShapeLayer.lineWidth = 15
    view.layer.addSublayer(timeLeftShapeLayer)
}

add your Label

func addTimeLabel() {
    timeLabel = UILabel(frame: CGRect(x: view.frame.midX-50 ,y: view.frame.midY-25, width: 100, height: 50))
    timeLabel.textAlignment = .center
    timeLabel.text = timeLeft.time
    view.addSubview(timeLabel)
}

at viewDidload set the endTime and add your CAShapeLayer to your view:

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = UIColor(white: 0.94, alpha: 1.0)
    drawBgShape()
    drawTimeLeftShape()
    addTimeLabel()
    // here you define the fromValue, toValue and duration of your animation
    strokeIt.fromValue = 0
    strokeIt.toValue = 1
    strokeIt.duration = timeLeft
    // add the animation to your timeLeftShapeLayer
    timeLeftShapeLayer.add(strokeIt, forKey: nil)
    // define the future end time by adding the timeLeft to now Date()
    endTime = Date().addingTimeInterval(timeLeft)
    timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
}

when updating the time

@objc func updateTime() {
    if timeLeft > 0 {
        timeLeft = endTime?.timeIntervalSinceNow ?? 0
        timeLabel.text = timeLeft.time
    } else {
        timeLabel.text = "00:00"
        timer.invalidate()
    }
}

you can use this extension to convert the degrees to radians and display time

extension TimeInterval {
    var time: String {
        return String(format:"%02d:%02d", Int(self/60),  Int(ceil(truncatingRemainder(dividingBy: 60))) )
    }
}
extension Int {
    var degreesToRadians : CGFloat {
        return CGFloat(self) * .pi / 180
    }
}

Sample project

Upvotes: 34

Darren
Darren

Reputation: 1712

Great piece of code and answer! Just thought I'd suggest the following. I was getting the time showing as 1:00 and then 0:60 which I think might be due to the "ceil" part of the code.

I changed it to the following (also wanting times less than 60 to show differently) and it seems to have removed the overlap of times.

For what it's worth ...

extension NSTimeInterval {
    var time:String {
        if self < 60 {
            return String(format: "%02d", Int(floor(self%60)))
        } else
        {
            return String(format: "%01d:%02d", Int(self/60), Int(floor(self%60)))
        }
    }
}

Upvotes: 2

Related Questions