Search code examples
swiftgeometryuiviewanimationprogressdrawrect

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


Solution

  • 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(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.