Skip to content

Instantly share code, notes, and snippets.

@toriaezunama
Created February 23, 2014 18:08
Show Gist options
  • Save toriaezunama/9174945 to your computer and use it in GitHub Desktop.
Save toriaezunama/9174945 to your computer and use it in GitHub Desktop.
///// View controller /////
#include "CGCustomPropertyLayer.h"
- (void)viewDidLoad
{
[super viewDidLoad];
_layer = [CGCustomPropertyLayer new];
[self.customLayerView.layer addSublayer:_layer];
_layer.bounds = self.customLayerView.bounds;
_layer.position = CGPointMake( CGRectGetWidth(_layer.bounds)/2, CGRectGetHeight(_layer.bounds)/2 );
[_layer setNeedsDisplay];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Animation
// Explicit
/*
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:kKeyPercentComplete];
anim.duration = 5;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.autoreverses = YES;
anim.repeatCount = MAXFLOAT;
anim.toValue = @(1.f);
[_layer addAnimation:anim forKey:kKeyPercentComplete];
/*/
// Implicit
_layer.percentComplete = 1; // can also use [_layer setValue:@(1) forKey:kKeyPercentComplete];
//*/
}
///// CGCustomPropertyLayer.h /////
#import <QuartzCore/QuartzCore.h>
@interface CGCustomPropertyLayer : CALayer
#define kKeyPercentComplete @"percentComplete"
@property (assign, nonatomic) CGFloat percentComplete;
@property (assign, nonatomic) BOOL nonDynamicProperty;
@end
///// CGCustomPropertyLayer.m /////
@implementation CGCustomPropertyLayer {
NSTimer *_timer; // Used to show that percentProperty's value is still interpolated even when needsDisplayForKey: returns NO for key kKeyPercentComplete
}
////////////// Implicit animation
/*
Firstly don’t @synthesize the animatable properties and instead mark them as @dynamic.
This is required because Core Animation does some magic under the hood to track changes to these properties and call appropriate methods on your layer.
Without this actionForKey: isn't called which means no animation is created for the property change.
*/
@dynamic percentComplete;
- (id)init {
self = [super init];
if (self) {
[self setup];
return self;
}
return nil;
}
// @dynamic properties are automatically copied over during initWithLayer:
// BUT non dynamic i.e. synthesized properties and instance properties aren't so in order
// to get these copied we override initWithLayer:
// This method gets called for each frame of animation.
// Core Animation makes a copy of the presentationLayer for each frame of the animation.
// By overriding this method we make sure our custom properties are correctly transferred to the copied-layer.
// This initializer is used to create shadow copies of layers, for example, for the presentationLayer method. Using this method in any other situation will produce undefined behavior. For example, do not use this method to initialize a new layer with an existing layer’s content.
//If you are implementing a custom layer subclass, you can override this method and use it to copy the values of instance variables into the new object. Subclasses should always invoke the superclass implementation.
//This method is the designated initializer for layer objects in the presentation layer.
- (id)initWithLayer:(id)layer
{
if (self = [super initWithLayer:layer]) {
if ([layer isKindOfClass:[CGCustomPropertyLayer class]]) {
CGCustomPropertyLayer *cpLayer = layer;
// Copy across ...
self.nonDynamicProperty = cpLayer.nonDynamicProperty;
}
}
return self;
}
- (void)setup {
// Because _percentComplete isn't declared (because it's @dynamic) we have to use the property setter to set an initial value which will create an implicit animation.
// There disable implicit animation for this first value
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.percentComplete = 0.0;
[CATransaction commit];
_timer = [NSTimer scheduledTimerWithTimeInterval:0.25
target:self
selector:@selector(timerCallback:)
userInfo:nil
repeats:YES];
[self addObserver:self
forKeyPath:kKeyPercentComplete
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
self.nonDynamicProperty = true;
}
// We also need to override needsDisplayForKey: to tell Core Animation that changes to our custom properties will require a redraw
// Called by [super init]
// Returning NO means drawInContext: won't be called. But the value it's self is still animated and can be accessed by layer.presentationLayer.percentComplete
+ (BOOL)needsDisplayForKey:(NSString*)key {
if ([key isEqualToString:kKeyPercentComplete]) {
return YES; // Set to NO to just animate the property
} else {
return [super needsDisplayForKey:key];
}
}
// Can't use defaultActionForKey: because there is no self pointer and hence we don't have access to the layer and it's properties
// Therefore we use actionForKey:
// Seems (*) to be called during the setter for percentComplete BEFORE the value is set so
// the value returned from self.percentComplete is still the OLD value
// (*) actionForKey: is called BEFORE observeValueForKeyPath: suggesting this to be the case
- (id<CAAction>)actionForKey:(NSString *)event
{
if ([event isEqualToString:kKeyPercentComplete]) {
NSLog( @"actionForKey:%@ = %f %@", event, self.percentComplete, self );
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:event];
anim.duration = 5;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.autoreverses = YES;
anim.repeatCount = MAXFLOAT;
anim.fromValue = @(self.percentComplete);
return anim;
} else {
return [super actionForKey:event];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:kKeyPercentComplete]) {
NSInteger type = [change[ NSKeyValueChangeKindKey ] intValue ];
// Value of observe object has changed
if( type == NSKeyValueChangeSetting ) {
// NSKeyValueChangeInsertion
// NSKeyValueChangeRemoval
// NSKeyValueChangeReplacement
// Do something
}
NSLog( @"Old: %@", change[ NSKeyValueChangeOldKey ] );
NSLog( @"New: %@", change[ NSKeyValueChangeNewKey ] );
CGCustomPropertyLayer *layer = object;
NSLog( @"PercentComplete: %f %@", layer.percentComplete, layer );
}
}
// Show percentComplete's value animating
- (void)timerCallback:(NSTimer *)timer
{
CGCustomPropertyLayer *layer = self.presentationLayer;
NSLog( @">>> %f", layer.percentComplete );
}
// Use percent complete to animate the path
- (void)drawInContext:(CGContextRef)context {
//NSLog( @"%@ drawInContext:%@ %f", self, context, self.percentComplete );
// Path set up
CGFloat outerRadius = MIN( CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds) ) /2;
CGFloat innerRadius = outerRadius * 0.5;
CGFloat size = self.percentComplete * 2 * M_PI;
CGPathRef path = [self segmentPathWithOuterRadius:outerRadius
innerRadius:innerRadius
angle:size];
CGContextAddPath(context, path);
CGContextSetFillColorWithColor(context,[UIColor redColor].CGColor);
CGContextFillPath(context);
}
- (CGPathRef)segmentPathWithOuterRadius:(CGFloat)outer
innerRadius:(CGFloat)inner
angle:(CGFloat)size
{
CGPoint centre = CGPointMake( CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds) /2);
UIBezierPath *path = [UIBezierPath bezierPath];
[path removeAllPoints];
[path moveToPoint:centre ];
[path addArcWithCenter:centre // centre
radius:outer
// 0 is +ve x axis and the sweep is clockwise
startAngle:0
endAngle:size
clockwise:YES];
[path addArcWithCenter:centre
radius:inner
// 0 is +ve x axis and the sweep is clockwise
startAngle:size
endAngle:0
clockwise:NO];
return path.CGPath;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment