Roi Mulia Roi Mulia - 6 months ago 141
Swift Question

Animate drawing of a circle

I'm looking for a way to animate the drawing of a circle. I have been able to create the circle, but it draws it all together.

Here is my

CircleView
class:

import UIKit

class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
}

required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


override func drawRect(rect: CGRect) {
// Get the Graphics Context
var context = UIGraphicsGetCurrentContext();

// Set the circle outerline-width
CGContextSetLineWidth(context, 5.0);

// Set the circle outerline-colour
UIColor.redColor().set()

// Create Circle
CGContextAddArc(context, (frame.size.width)/2, frame.size.height/2, (frame.size.width - 10)/2, 0.0, CGFloat(M_PI * 2.0), 1)

// Draw
CGContextStrokePath(context);
}
}


And here is how I add it to the view hierarchy in my view controller:

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)
}


Is there a way to animate the drawing of the circle over 1 second?

Example, part way through the animation it would look something like the blue line in this image:

partial animation

Answer

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 the subregion of the path to stroke. The value in this property indicates the relative point along the path at which to finish stroking while the strokeStart property defines the starting point. A value of 0.0 represents the beginning of the path while a value of 1.0 represents the end of the path. Values in between are interpreted linearly along the 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(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.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: "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.addAnimation(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.

Comments