More CADisplayLink

Hello to everyone coming from iDevBlogADay!

Since this is my first post to be pulled into the site, I’ll take just a quick moment to introduce myself.  My name is Pat Zearfoss and I’m one of the lead iOS developers at Mindgrub Technologies, a development shop in Baltimore, MD, USA.  We’re primarily a work-for-hire shop, but we do have a couple products we’ve released on our own.  I’ve been working on iOS since the SDK became public and because of the nature of my work at Mindgrub, I’ve had the opportunity to work on a variety of projects, large and small.

This site is a collection of thoughts on iOS development, usually spurned from a project I’m working on.  Lately I’ve been spending a lot of time in CoreAnimation, so as a result you’ll find a lot on that topic.  I usually try to jot down things that weren’t immediately obvious to me as I work through a project, usually figuring that if I had to really dig to find a solution, someone else probably will to.

I was rattling my brain to try and come up with a good introductory topic for my first iDevBlogADay post and I eventually settled upon doing another post about CADisplayLink.  Not that this site gets a ton of traffic, but CADisplayLink tops my search terms daily.  Documentation around the web on it is scarce as it’s a pretty niche topic, although highly useful when the animation you want to create in an iOS app is something not easily handled by transitions or stock animations.

So what is a CADisplayLink.  Directly from Apple:

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

CADisplayLink works very much like an NSTimer.  You can set it to call a method on every tick, but instead of setting a time interval, the time interval is determined by the screen’s refresh rate.  When you’re ready to use the display link you add it to the run loop, and the method you handed to it will fire until you call the “invalidate” method on it.  CADisplayLink lives in the QuartzCore framework.

In an earlier post, I showed how to draw a simple line on a CALayer using CADisplayLink, drawing an additional pixel every time.  Today I’m going to show you how to draw a circle fanning out (similar to how the pie charts render in Mint.com) and setting a desired time duration for that animation. To get started, we’re going to create a new project with a single view controller and UIView subclass.  Here’s my project layout:

We’ll also need to add the QuartzCore framework to the project.  (Target settings -> Build Phases):

Next in viewDidLoad of our view controller, we need to create a new instance of our UIView subclass and add it to the view:


- (void)viewDidLoad
{
    [super viewDidLoad];
    CircleDisplayView *circleView =
        [[CircleDisplayView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:circleView];
    [circleView release];
}

Now onto our custom view. This is where we’ll be using the CADisplayLink. Here’s my .h file:

#import <QuartzCore/QuartzCore.h>

@interface CircleDisplayView : UIView
{
    CADisplayLink *displayLink;

    BOOL animationRunning;
    NSTimeInterval drawDuration;
    CFTimeInterval lastDrawTime;
    CGFloat drawProgress;
}
@end

Our view only needs to keep track of a couple things. First we need the CADisplayLink. We’ll need to keep track of it during drawing.  We’ll also need a flag to know whether our animation is running (animationRunning) and some variables to keep track of timestamps(lastDrawTime), the drawing progress (drawProgress), as well as the amount of time we want the animation to run for (drawDuration)

In the initializer, all we’ll need to do is get an instance of CADisplayLink and set the amount of time we want for the animation:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        displayLink = [CADisplayLink displayLinkWithTarget:self
                                                  selector:@selector(setNeedsDisplay)];
        drawDuration = 3.0;
    }
    return self;
}

The method we’ll be using in the CADisplayLink is simply setNeedsDisplay.  So, while the animation is running, on every tick of the display link we’ll be asking the view to redraw itself.  All the magic then happens in drawRect:

- (void)drawRect:(CGRect)rect
{
    if (!animationRunning)
    {
        [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        animationRunning = YES;
        return;
    }

    if (lastDrawTime == 0)
    {
        lastDrawTime = displayLink.timestamp;
        return;
    }

    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(ctx, [[UIColor blueColor] CGColor]);

    CFTimeInterval elapsedTime = displayLink.timestamp - lastDrawTime;
    NSLog(@"elapsed %f", elapsedTime);

    CGFloat radiansToDraw = drawProgress + ((2 * M_PI) / drawDuration) * elapsedTime;

    NSLog(@"drawing %f radians", radiansToDraw);

    CGContextMoveToPoint(ctx, self.center.x, self.center.y);
    CGContextAddLineToPoint(ctx, self.center.x + 100, self.center.y);
    CGContextAddArc(ctx, self.center.x, self.center.y, 100, 0, radiansToDraw, 0);
    CGContextClosePath(ctx);
    CGContextFillPath(ctx);

    lastDrawTime = displayLink.timestamp;
    drawProgress = radiansToDraw;

    if (radiansToDraw > 2 * M_PI)
    {
        NSLog(@"Invalidate display link");
        [displayLink invalidate];
        animationRunning = NO;
        lastDrawTime = 0;
    }

}

Most of this code is standard drawing code: adding lines and arcs, so I’m not going to go into great detail on those, but rather focus on the bits having to do with making the circle drawing occur progressively.

The first thing to notice is that on the first (and second) pass through drawRect, no actual drawing occurs.  The first pass occurs when the view naturally tries to render itself.  On this pass all we need to do is add the displayLink to the run loop and return.

Second pass we get caught in the second if statement, ensuring that we seed lastDrawTime.  We need only know about the differences in two display link timestamps, so this is necessary.

Finally on the third pass we begin drawing.  It’s important to remember that the whole view gets refreshed every pass of drawRect, so all we essentially need to do is add some radians to the arc drawn every pass.  When we notice that the amount of radians drawn exceeds 2π we know the circle is complete and reset the display link.

And that’s it!  If you run that code, you should see the circle fan out at a constant rate.  You could clearly make this slicker by seeding the display link on didAddToSuperview or adding an easing function to the calculation for radiansToDraw.  I hope this has been useful to some folks out there.  As always I’ll post the code to github so you can check it out.

Thanks and happy coding!

Advertisements

Posted on September 2, 2011, in Code, idevblogaday and tagged , , , . Bookmark the permalink. 8 Comments.

  1. Thank you for nice tutorial. I’ve implemented my animation with NSTimer, but now I see that approach with CADisplayLink is very similar to it, but is told to be better. So now I can convert it to correct implementation with help of this information.

  2. I seem to be having trouble implementing this in my case. I have a UIView that I’m trying to draw within using CADisplayLink as a timer. Unfortunately the display link doesn’t ever seem to trigger setNeedsDisplay, so drawRect is never called.

    I have this in my init method:
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)];

    This is the line I use to add the display link to the view:
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

    I also as your code suggests use a boolean to track whether or not I’m animating. That’s all working correctly and it is hitting the section to add the displayLink to the main run loop. It just never seems to get farther than that. I’ve tried moving the displayLink work up into my controller rather than in the view itself and it worked ok. I’d rather keep all of the drawing logic within the view though so I’d like to see if I can figure out what isn’t working correctly.

  3. Just thought I’d add to my prior comment. I moved just the initialization of the displayLink up into my controller. So the controller in viewDidLoad sets the displayLink on the view I’m drawing in. The view is still responsible for adding the displayLink to the run loop and then removing it when done. For some reason it seems that when I initially had the initialization line within the view referencing “self” as the displayLink target as well as the “setNeedsDisplay” selector isn’t working. At first I thought this had something to do with the fact that view hadn’t yet fully initialized but moving the displayLink initialization into the method that actually adds it to the run loop didn’t make any difference.

  4. Ah, sorry for the confusion (and the numerous posts). It turns out it was because my initWithFrame was never being called due to the view being built from the InterfaceBuilder. I’ve corrected that and now initialize my iVars within the view inside of awakeFromNib, all better!

  5. Hi Pat,

    I have been trying to adapt your example to my CALayer animation application. It seems to work displaying once, but not again. I create a custom UIView in the ViewController and call a start method in the custom UIView which just calls the run method using performSelectorInBackground. The run method dynamically adds CALayers to the custom UIView and then move them with position and animate by setting contensrect on the image. I want to redisplay every time I adjust the CALayers (no more than 10 for each animation.

    Here is the code, perhaps you would comment:

    – (void) start {

    [self performSelectorInBackground:@selector(run)withObject:nil];

    }

    – (void) run {
    // urlstring = @”http://www.javelin21.mobi/servlets/gadloader?=75235″;

    PGM = [[[GadsMonitor alloc] init: @”http://www.javelin21.com/servlets/gadsloader?=75235″ :_displaySizeW :_displaySizeH]autorelease];

    while(!PGM.adsdone){
    [NSThread sleepForTimeInterval:2.0];

    }

    GVarray = [PGM.PGVarray mutableCopy];

    Gad * readyGad = (Gad *) [GVarray objectAtIndex:0];
    if (readyGad != nil)
    [readyGad getImages];

    while (!readyGad.gdone) {
    [NSThread sleepForTimeInterval:2.0];
    [readyGad checkImages];
    }

    displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(setAction)];

    rstop = NO;

    while (!rstop) {
    [self rotateGames];

    stop = NO;
    while (!stop) {
    [self gameAction];
    [self paint];
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    rstop = NO;

    [NSThread sleepForTimeInterval:stime];

    rtime = (rtime +runGad.gslp);
    ltime += runGad.gslp;
    jtime += runGad.gslp;
    vtime += runGad.gslp;
    ftime += runGad.gslp;
    ttime += runGad.gslp;
    Stime += runGad.gslp;

    if ((runTime += runGad.gslp) >= runGad.grun) {
    stop = YES;
    }
    }

    stop = NO;
    }

    }

  6. Thanks for this! The existing documentation and code samples for CADisplayLink is quite thin, and using this article I was able to get it up and running quickly.

  1. Pingback: More CADisplayLink | Pat Zearfoss ← Delmarva News

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: