Blog Archives

Another CoreAnimation Protip

If you’re building your interface using CALayers, beware the following weirdness when sublayers are added to a parent layer.  I had a parent layer, with a bunch of sublayers being added and removed during the lifetime of the view.  I didn’t want any animations when that occurs, but I was still noticing a fade in / fade out action going on.

I had already disabled the kCAOrderIn and kCAOrderOut action keys on the sublayers, but I didn’t realize I also had to disable the sublayers action on the parent layer.  Beware that the sublayers action is also fired when you call [CALayer addSublayer:] and [CALayer removeFromSuperlayer].

Advertisements

Objective-C Quickie – Block variables

Blocks are becoming more and more preferred in iOS and Objective-c, with most of the UIView animations becoming block based and methods added to the NS collection classes to use blocks for enumeration. Blocks can also be stored as variables and used over and over again. This is immensely useful when subscribing to a DRY method of coding.

Below is some code I’m using to apply transformations to CALayers. I’ve got three sets of them, and since I simply want to apply the same transformation to them, I can write the block for creating and applying the transformation once, and use it on all three sets:

// this is a block variable
id transBlock = ^(id obj, BOOL *stop)
{
    CALayer *layer = (CALayer *)obj;
    CGAffineTransform trans = CGAffineTransformConcat([layer affineTransform],                                                       CGAffineTransformMakeTranslation(delta * 0.7, 0));
    [layer setAffineTransform:trans];
};

[displayedBars enumerateObjectsUsingBlock:transBlock];
[displayedBarLabels enumerateObjectsUsingBlock:transBlock];
[displayedAxisLabels enumerateObjectsUsingBlock:transBlock];

Drawing with CADisplayLink

For 90% of the animations you’ll ever need to do CoreAnimation or UIView animations are enough. Sometimes, the thing you want to animate can’t be handled by simple property based transitions or by moving an object along a path. Enter CADisplayLink:

From the CADisplayLink class reference:

A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display.

The purposes that I need, I want to animate a circle similarly to how flex charts draws pie charts (a “swipe open” kind of animation). For the demo, though, I’ll simply use the CADisplayLink to animate a line extending across the screen on a CALayer using it’s drawInContext: method.

First the main view that contains my CALayer subclass:

//  MainView.h

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import "AnimationLayer.h"

@interface MainView : UIView 
{
    AnimationLayer *alayer;
}

@end
//  MainView.m

#import "MainView.h"

@implementation MainView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}

- (void)didMoveToSuperview
{
    if ([self superview])
    {
                
        alayer = [[AnimationLayer alloc] init];
        alayer.frame = self.frame;
        [self.layer addSublayer:alayer];
        
    }
}

And next the CALayer that will be animated:

//  AnimationLayer.h

#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface AnimationLayer : CALayer 
{
    CADisplayLink *displayLink;
}

@end
//  AnimationLayer.m
#import "AnimationLayer.h"

static bool _running;

@implementation AnimationLayer

- (id)init
{
    self = [super init];
    if (self)
    {
        displayLink = [[CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)] retain];
        [self setNeedsDisplayOnBoundsChange:YES];
    }
    
    return self;
}

static CGPoint lastPoint = {0, 0};
- (void)drawInContext:(CGContextRef)ctx
{
    if (!_running)
    {
        [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        _running = YES;
        return;
    }

    CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]);
    
    CGRect rect = CGContextGetClipBoundingBox(ctx);
    
    CGContextMoveToPoint(ctx, 0, 0);
    lastPoint.x = lastPoint.y += 1;
    CGContextAddLineToPoint(ctx, lastPoint.x, lastPoint.y);
    CGContextStrokePath(ctx);
    
    if (lastPoint.x == rect.size.width)
    {
        [displayLink invalidate];
        _running = NO
    }
}

@end

The main view is pretty uninteresting as it only creates a new CALayer and adds it as a sublayer of itself.

The animation layer holds the CADisplayLink as an Ivar and initializes it with the layer’s drawInContext: method.

We need some kind of flag to tell the CADisplayLink when to stop updating, for this I used a static BOOL _running. On the first pass of drawInContext, we’ll add the display link to the main run loop, at which point it will start calling the specified selector. We’ll set the _ruinning flag to true and return. On every subsequent pass of drawInContext: we’ll draw a longer line running diagonally. At the end we’ll check the condition to invalidate the CADisplayLink; in this case I’ll check to see whether the line has fully crossed the screen. Once we invalidate the display link the animation will stop.

This is a simple example, but it shows how you can animate an object using pure drawing for the times when property animations simply won’t do.

Another CoreAnimation Gotcha

As I said before, the documentation available for CoreAnimation is a bit lacking.  This one had me scratching my head for a time today.

For my current project, I have several layers that should have some of their default animation actions disabled.  This is pretty easy to do by handing a dictionary with the desired keys set to an instance of NSNull.  For example:


NSDictionary *actions = [NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], @"position", nil];

CALayer *aLayer = [[CALayer alloc] init];

aLayer.actions = actions;

Under a different set of circumstances I need to reenable the animation.  This, too, is simple to do using CABasicAnimation:


CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];

animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];

[aLayer addAnimation:animation forKey:@"position"];

This code runs fine, until you have the need to intercept the animation while it’s executing.  A CALayer maintains two versions of itself.

The first is the model layer ([aLayer modelLayer]), which gets written to when you change a property on it.  For example, setting the position to {10, 10} writes this information to the model layer, and is present throughout the animation.

The second is the presentation layer ([aLayer presentationLayer]), which hold the location of the CALayer as it’s presented on the screen.  If you want to interact with the layer as it’s moving, you need to access this layer.  If you’re using the default actions on the layer, this all works as expected.  Once you replace the default action, there’s another step you have to take which is not clearly stated in the documentation:

animation.fromValue = [NSValue valueWithCGPoint:start];
animation.toValue = [NSValue valueWithCGPoint:end];

Without specifying a start and end point, the action is unable to interpolate intermediate values during the animation.  If you inspect the presentation layer without specifying a start and end, you’ll find that the values for the property you’re looking for are the same as those in the model layer.