Animated CAShapeLayer Pie

I know this question has long been answered, but I don't quite think this is a good case for CAShapeLayer and CAKeyframeAnimation. Core Animation has the power to do animation tweening for us. Here's a class (with a wrapping UIView, if you like) that I use to accomplish the effect pretty well.

The layer subclass enables implicit animation for the progress property, but the view class wraps its setter in a UIView animation method. The interesting (and ultimately useful) side effect of using a 0.0 length animation with UIViewAnimationOptionBeginFromCurrentState is that each animation cancels out the previous one, leading to a smooth, fast, high-framerate pie, like this (animated) and this (not animated, but incremented).

DZRoundProgressView.h

@interface DZRoundProgressLayer : CALayer

@property (nonatomic) CGFloat progress;

@end

@interface DZRoundProgressView : UIView

@property (nonatomic) CGFloat progress;

@end

DZRoundProgressView.m

#import "DZRoundProgressView.h"
#import <QuartzCore/QuartzCore.h>

@implementation DZRoundProgressLayer

// Using Core Animation's generated properties allows
// it to do tweening for us.
@dynamic progress;

// This is the core of what does animation for us. It
// tells CoreAnimation that it needs to redisplay on
// each new value of progress, including tweened ones.
+ (BOOL)needsDisplayForKey:(NSString *)key {
    return [key isEqualToString:@"progress"] || [super needsDisplayForKey:key];
}

// This is the other crucial half to tweening.
// The animation we return is compatible with that
// used by UIView, but it also enables implicit
// filling-up-the-pie animations.
- (id)actionForKey:(NSString *) aKey {
    if ([aKey isEqualToString:@"progress"]) {
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:aKey];
        animation.fromValue = [self.presentationLayer valueForKey:aKey];
        return animation;
    }
    return [super actionForKey:aKey];
}

// This is the gold; the drawing of the pie itself.
// In this code, it draws in a "HUD"-y style, using
// the same color to fill as the border.
- (void)drawInContext:(CGContextRef)context {
    CGRect circleRect = CGRectInset(self.bounds, 1, 1);

    CGColorRef borderColor = [[UIColor whiteColor] CGColor];
    CGColorRef backgroundColor = [[UIColor colorWithWhite: 1.0 alpha: 0.15] CGColor];

    CGContextSetFillColorWithColor(context, backgroundColor);
    CGContextSetStrokeColorWithColor(context, borderColor);
    CGContextSetLineWidth(context, 2.0f);

    CGContextFillEllipseInRect(context, circleRect);
    CGContextStrokeEllipseInRect(context, circleRect);

    CGFloat radius = MIN(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect));
    CGPoint center = CGPointMake(radius, CGRectGetMidY(circleRect));
    CGFloat startAngle = -M_PI / 2;
    CGFloat endAngle = self.progress * 2 * M_PI + startAngle;
    CGContextSetFillColorWithColor(context, borderColor);
    CGContextMoveToPoint(context, center.x, center.y);
    CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0);
    CGContextClosePath(context);
    CGContextFillPath(context);

    [super drawInContext:context];
}

@end

@implementation DZRoundProgressView
+ (Class)layerClass {
    return [DZRoundProgressLayer class];
}

- (id)init {
    return [self initWithFrame:CGRectMake(0.0f, 0.0f, 37.0f, 37.0f)];
}

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        self.opaque = NO;
        self.layer.contentsScale = [[UIScreen mainScreen] scale];
        [self.layer setNeedsDisplay];
    }
    return self;
}

- (void)setProgress:(CGFloat)progress {
    [(id)self.layer setProgress:progress];
}

- (CGFloat)progress {
    return [(id)self.layer progress];
}

@end

I suggest you make a keyframe animation instead:

pie.bounds = CGRectMake(-0.5 * radius,
                        -0.5 * radius,
                        radius,
                        radius);

NSMutableArray *values = [NSMutableArray array];
for (int i = 0; i < nrSteps + 1; i++)
{
    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:CGPointZero];
    [path addLineToPoint:CGPointMake(radius * cosf(startAngle),
                                     radius * sinf(startAngle))];
    [path addArcWithCenter:...
                  endAngle:startAngle + i * (endAngle - startAngle) / nrSteps
                           ...];
    [path closePath];
    [values addObject:(__bridge id)path.CGPath];
}

Basic animation is good for scalars/vectors. But do you want it to interpolate your paths?


Quick copy/paste Swift translation of this excellent Objective-C answer.

class ProgressView: UIView {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        genericInit()
    }

    private func genericInit() {
        self.opaque = false;
        self.layer.contentsScale = UIScreen.mainScreen().scale
        self.layer.setNeedsDisplay()
    }

    var progress : CGFloat = 0 {
        didSet {
            (self.layer as! ProgressLayer).progress = progress
        }
    }

    override class func layerClass() -> AnyClass {
        return ProgressLayer.self
    }

    func updateWith(progress : CGFloat) {
        self.progress = progress
    }
}

class ProgressLayer: CALayer {

    @NSManaged var progress : CGFloat

    override class func needsDisplayForKey(key: String!) -> Bool{

        return key == "progress" || super.needsDisplayForKey(key);
    }

    override func actionForKey(event: String!) -> CAAction! {

        if event == "progress" {
            let animation = CABasicAnimation(keyPath: event)
            animation.duration = 0.2
            animation.fromValue = self.presentationLayer().valueForKey(event)
            return animation
        }

        return super.actionForKey(event)
    }

    override func drawInContext(ctx: CGContext!) {

        if progress != 0 {

            let circleRect = CGRectInset(self.bounds, 1, 1)
            let borderColor = UIColor.whiteColor().CGColor
            let backgroundColor = UIColor.clearColor().CGColor

            CGContextSetFillColorWithColor(ctx, backgroundColor)
            CGContextSetStrokeColorWithColor(ctx, borderColor)
            CGContextSetLineWidth(ctx, 2)

            CGContextFillEllipseInRect(ctx, circleRect)
            CGContextStrokeEllipseInRect(ctx, circleRect)

            let radius = min(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect))
            let center = CGPointMake(radius, CGRectGetMidY(circleRect))
            let startAngle = CGFloat(-(M_PI/2))
            let endAngle = CGFloat(startAngle + 2 * CGFloat(M_PI * Double(progress)))

            CGContextSetFillColorWithColor(ctx, borderColor)
            CGContextMoveToPoint(ctx, center.x, center.y)
            CGContextAddArc(ctx, center.x, center.y, radius, startAngle, endAngle, 0)
            CGContextClosePath(ctx)
            CGContextFillPath(ctx)
        }
    }
}