Animate drawing of a circle Animate drawing of a circle swift swift

Animate drawing of a circle


The easiest way to do this is to use the power of core animation to do most of the work for you. To do that, we'll have to move your circle drawing code from your drawRect function to a CAShapeLayer. Then, we can use a CABasicAnimation to animate CAShapeLayer's strokeEnd property from 0.0 to 1.0. strokeEnd is a big part of the magic here; from the docs:

Combined with the strokeStart property, this property defines thesubregion of the path to stroke. The value in this property indicatesthe relative point along the path at which to finish stroking whilethe strokeStart property defines the starting point. A value of 0.0represents the beginning of the path while a value of 1.0 representsthe end of the path. Values in between are interpreted linearly alongthe path length.

If we set strokeEnd to 0.0, it won't draw anything. If we set it to 1.0, it'll draw a full circle. If we set it to 0.5, it'll draw a half circle. etc.

So, to start, lets create a CAShapeLayer in your CircleView's init function and add that layer to the view's sublayers (also be sure to remove the drawRect function since the layer will be drawing the circle now):

let circleLayer: CAShapeLayer!override init(frame: CGRect) {    super.init(frame: frame)    self.backgroundColor = UIColor.clearColor()        // Use UIBezierPath as an easy way to create the CGPath for the layer.    // The path should be the entire circle.    let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(Double.pi * 2.0), clockwise: true)        // Setup the CAShapeLayer with the path, colors, and line width    circleLayer = CAShapeLayer()    circleLayer.path = circlePath.CGPath    circleLayer.fillColor = UIColor.clearColor().CGColor    circleLayer.strokeColor = UIColor.redColor().CGColor    circleLayer.lineWidth = 5.0;        // Don't draw the circle initially    circleLayer.strokeEnd = 0.0        // Add the circleLayer to the view's layer's sublayers    layer.addSublayer(circleLayer)}

Note: We're setting circleLayer.strokeEnd = 0.0 so that the circle isn't drawn right away.

Now, lets add a function that we can call to trigger the circle animation:

func animateCircle(duration: NSTimeInterval) {    // We want to animate the strokeEnd property of the circleLayer    let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))    // Set the animation duration appropriately    animation.duration = duration    // Animate from 0 (no circle) to 1 (full circle)    animation.fromValue = 0    animation.toValue = 1    // Do a linear animation (i.e. the speed of the animation stays the same)    animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)    // Set the circleLayer's strokeEnd property to 1.0 now so that it's the    // right value when the animation ends.    circleLayer.strokeEnd = 1.0    // Do the actual animation    circleLayer.add(animation, forKey: "animateCircle")}

Then, all we need to do is change your addCircleView function so that it triggers the animation when you add the CircleView to its superview:

func addCircleView() {    let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)     var circleWidth = CGFloat(200)     var circleHeight = circleWidth        // Create a new CircleView     var circleView = CircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight))     view.addSubview(circleView)     // Animate the drawing of the circle over the course of 1 second     circleView.animateCircle(1.0)}

All that put together should look something like this:

circle animation

Note: It won't repeat like that, it'll stay a full circle after it animates.


Mikes answer updated for Swift 3.0

var circleLayer: CAShapeLayer!override init(frame: CGRect) {    super.init(frame: frame)    self.backgroundColor = UIColor.clear    // Use UIBezierPath as an easy way to create the CGPath for the layer.    // The path should be the entire circle.    let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)    // Setup the CAShapeLayer with the path, colors, and line width    circleLayer = CAShapeLayer()    circleLayer.path = circlePath.cgPath    circleLayer.fillColor = UIColor.clear.cgColor    circleLayer.strokeColor = UIColor.red.cgColor    circleLayer.lineWidth = 5.0;    // Don't draw the circle initially    circleLayer.strokeEnd = 0.0    // Add the circleLayer to the view's layer's sublayers    layer.addSublayer(circleLayer)} required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")}func animateCircle(duration: TimeInterval) {    // We want to animate the strokeEnd property of the circleLayer    let animation = CABasicAnimation(keyPath: "strokeEnd")    // Set the animation duration appropriately    animation.duration = duration    // Animate from 0 (no circle) to 1 (full circle)    animation.fromValue = 0    animation.toValue = 1    // Do a linear animation (i.e The speed of the animation stays the same)    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)    // Set the circleLayer's strokeEnd property to 1.0 now so that it's the    // Right value when the animation ends    circleLayer.strokeEnd = 1.0    // Do the actual animation    circleLayer.add(animation, forKey: "animateCircle")}

To call the function:

func addCircleView() {    let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)    var circleWidth = CGFloat(200)    var circleHeight = circleWidth    // Create a new CircleView    let circleView = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))    //let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))    view.addSubview(circleView)    // Animate the drawing of the circle over the course of 1 second    circleView.animateCircle(duration: 1.0)}


Mike's answer is great! Another nice and simple way to do it is use drawRect combined with setNeedsDisplay(). It seems laggy, but its not :-)enter image description here

We want to draw a circle starting from the top, which is -90° and ends at 270°. The circle's center is (centerX, centerY), with a given radius. CurrentAngle is the current angle of the end-point of the circle, going from minAngle (-90) to maxAngle (270).

// MARK: Propertieslet centerX:CGFloat = 55let centerY:CGFloat = 55let radius:CGFloat = 50var currentAngle:Float = -90let minAngle:Float = -90let maxAngle:Float = 270

In drawRect, we specify how the circle is supposed to display :

override func drawRect(rect: CGRect) {    let context = UIGraphicsGetCurrentContext()    let path = CGPathCreateMutable()    CGPathAddArc(path, nil, centerX, centerY, radius, CGFloat(GLKMathDegreesToRadians(minAngle)), CGFloat(GLKMathDegreesToRadians(currentAngle)), false)    CGContextAddPath(context, path)    CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)    CGContextSetLineWidth(context, 3)    CGContextStrokePath(context)}

The problem is that right now, as currentAngle is not changing, the circle is static, and doesn't even show, as currentAngle = minAngle.

We then create a timer, and whenever that timer fires, we increase currentAngle. At the top of your class, add the timing between two fires :

let timeBetweenDraw:CFTimeInterval = 0.01

In your init, add the timer :

NSTimer.scheduledTimerWithTimeInterval(timeBetweenDraw, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)

We can add the function that will be called when the timer fires :

func updateTimer() {    if currentAngle < maxAngle {        currentAngle += 1    }}

Sadly, when running the app, nothing displays because we did not specify the system that it should draw again. This is done by calling setNeedsDisplay(). Here is the updated timer function :

func updateTimer() {    if currentAngle < maxAngle {        currentAngle += 1        setNeedsDisplay()    }}

___

All the code you need is summed-up here :

import UIKitimport GLKitclass CircleClosing: UIView {    // MARK: Properties    let centerX:CGFloat = 55    let centerY:CGFloat = 55    let radius:CGFloat = 50    var currentAngle:Float = -90    let timeBetweenDraw:CFTimeInterval = 0.01    // MARK: Init    required init?(coder aDecoder: NSCoder) {        super.init(coder: aDecoder)        setup()    }    override init(frame: CGRect) {        super.init(frame: frame)        setup()    }    func setup() {        self.backgroundColor = UIColor.clearColor()        NSTimer.scheduledTimerWithTimeInterval(timeBetweenDraw, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)    }    // MARK: Drawing    func updateTimer() {        if currentAngle < 270 {            currentAngle += 1            setNeedsDisplay()        }    }    override func drawRect(rect: CGRect) {        let context = UIGraphicsGetCurrentContext()        let path = CGPathCreateMutable()        CGPathAddArc(path, nil, centerX, centerY, radius, -CGFloat(M_PI/2), CGFloat(GLKMathDegreesToRadians(currentAngle)), false)        CGContextAddPath(context, path)        CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)        CGContextSetLineWidth(context, 3)        CGContextStrokePath(context)    }}

If you want to change the speed, just modify the updateTimer function, or the rate at which this function is called. Also, you might want to invalidate the timer once the circle is complete, which I forgot to do :-)

NB: To add the circle in your storyboard, just add a view, select it, go to its Identity Inspector, and as Class, specify CircleClosing.

Cheers! bRo