Core animation progress callback

I’ve finally developed a solution for this problem.

Essentially I wish to be called back for every frame and do what I need to do.

There’s no obvious way to observe the progress of an animation, however it is actually possible:

  • Firstly we need to create a new subclass of CALayer that has an animatable property called ‘progress’.

  • We add the layer into our tree, and then create an animation that will drive the progress value from 0 to 1 over the duration of the animation.

  • Since our progress property can be animated, drawInContext is called on our sublass for every frame of an animation. This function doesn’t need to redraw anything, however it can be used to call a delegate function 🙂

Here’s the class interface:

@protocol TAProgressLayerProtocol <NSObject>

- (void)progressUpdatedTo:(CGFloat)progress;

@end

@interface TAProgressLayer : CALayer

@property CGFloat progress;
@property (weak) id<TAProgressLayerProtocol> delegate;

@end

And the implementation:

@implementation TAProgressLayer

// We must copy across our custom properties since Core Animation makes a copy
// of the layer that it's animating.

- (id)initWithLayer:(id)layer
{
    self = [super initWithLayer:layer];
    if (self) {
        TAProgressLayer *otherLayer = (TAProgressLayer *)layer;
        self.progress = otherLayer.progress;
        self.delegate = otherLayer.delegate;
    }
    return self;
}

// Override needsDisplayForKey so that we can define progress as being animatable.

+ (BOOL)needsDisplayForKey:(NSString*)key {
    if ([key isEqualToString:@"progress"]) {
        return YES;
    } else {
        return [super needsDisplayForKey:key];
    }
}

// Call our callback

- (void)drawInContext:(CGContextRef)ctx
{
    if (self.delegate)
    {
        [self.delegate progressUpdatedTo:self.progress];
    }
}

@end

We can then add the layer to our main layer:

TAProgressLayer *progressLayer = [TAProgressLayer layer];
progressLayer.frame = CGRectMake(0, -1, 1, 1);
progressLayer.delegate = self;
[_sceneView.layer addSublayer:progressLayer];

And animate it along with the other animations:

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"progress"];
anim.duration = 4.0;
anim.beginTime = 0;
anim.fromValue = @0;
anim.toValue = @1;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;

[progressLayer addAnimation:anim forKey:@"progress"];

Finally, the delegate will be called back as the animation progresses:

- (void)progressUpdatedTo:(CGFloat)progress
{
    // Do whatever you need to do...
}

Leave a Comment