20
Oct
2009
 

Marching Ants With Core Animation

by Matt Long

Marching AntsOur Core Animation book should be available by the end of the year. Go ahead and pre-order it now at Amazon if you would like ;-). When we started writing for Addison-Wesley back in September of 2008, I had no idea how long to expect it to take to finish a technical book as this was my first. One thing I discovered though, is that it is when you are about ready to go to production you start to realize all of the things that you probably should have added to the book, but didn’t think of in time. This blog post will cover one such item as a way to make up for not thinking of it in time. I may include this in a second edition if there is one, but consider this one a freebie.

One of the Core Animation layers available as of Snow Leopard and iPhone OS 3.0, CAShapeLayer provides some interesting attributes that are animatable. The CAShapeLayer enables you to create a layer that renders any arbitrary shape you specify using a path, a CGPathRef. It also enables you to specify a stroke pattern for the stroke that outlines the shape. A stroke is optional, but if you do specify one you may also determine the pattern for it. I do point this part out in the book in Chapter 10, by the way. You use the method, -setLineDashPattern which takes an NSArray of integers. These integers specify painted segments verses unpainted segments. What this means is that if you specify 10, 5 in your array, for example, you will see 10 units of painted segment and 5 units unpainted. Notice I use the term units as opposed to pixels. This is due to the fact that we are to start getting used to the idea of resolution independence. Pixels don’t matter in the same way any more.

The pattern example I used is very simple. You could easily specify something much more complex, say 10, 72, 55, 2, 146, etc. for example. This would mean 10 painted units, followed by 72 unpainted units, followed by 55 painted units, followed by 2 unpainted units, followed by 146 painted units–on and on. You get the picture.

Now for the part I didn’t mention in the book. You can animate this stroke pattern. If you’ve ever used any drawing program you realize that when a rectangular area of the image is in a seleted mode, a dashed pattern stroke outlines that area and the pattern will move like marching ants. To achieve this effect manually actually takes quite a bit of effort, but is quite trivial when done with Core Animation.

What makes it so is another little property of the CAShaperLayer called lineDashPhase. This property specifies the phase of the stroke pattern. The default is zero. Animate this property to see the marching ants effect. If you want the animation to loop perfectly, set the toValue in the animation to the sum of all of the values specified in the lineDashPattern array. The code to animate the lineDashPhase should look something like the following:

- (IBAction)toggleMarching:(id)sender;
{
    if ([shapeLayer animationForKey:@"linePhase"])
        [shapeLayer removeAnimationForKey:@"linePhase"];
    else {
        CABasicAnimation *dashAnimation;
        dashAnimation = [CABasicAnimation 
                         animationWithKeyPath:@"lineDashPhase"];
        
        [dashAnimation setFromValue:[NSNumber numberWithFloat:0.0f]];
        [dashAnimation setToValue:[NSNumber numberWithFloat:15.0f]];
        [dashAnimation setDuration:0.75f];
        [dashAnimation setRepeatCount:10000];
        
        [shapeLayer addAnimation:dashAnimation forKey:@"linePhase"];
        
    }
}

This is the code we would use to animate a stroke pattern of 10, 5. Notice the sum of those two is 15, which is what the call to setToValue takes in the sample code for the animation. This is important. If the phase doesn’t match the stroke pattern, the animation will jerk back to the starting position. We want the animation to be smooth and seamless.

We set up our CAShaperLayer using the following code:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Create the shape layer
    shapeLayer = [CAShapeLayer layer];
    CGRect shapeRect = CGRectMake(0.0f, 0.0f, 200.0f, 100.0f);
    [shapeLayer setBounds:shapeRect];
    [shapeLayer setPosition:CGPointMake(160.0f, 140.0f)];
    [shapeLayer setFillColor:[[UIColor clearColor] CGColor]];
    [shapeLayer setStrokeColor:[[UIColor blackColor] CGColor]];
    [shapeLayer setLineWidth:1.0f];
    [shapeLayer setLineJoin:kCALineJoinRound];
    [shapeLayer setLineDashPattern:
     [NSArray arrayWithObjects:[NSNumber numberWithInt:10], 
      [NSNumber numberWithInt:5], 
      nil]];
    
    
    // Setup the path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, shapeRect);
    [shapeLayer setPath:path];
    CGPathRelease(path);
    
    // Set the layer's contents
    [shapeLayer setContents:(id)[[UIImage imageNamed:@"balloon.jpg"] CGImage]];
    
    [[[self view] layer] addSublayer:shapeLayer];
    
}

Set the fill color to clear color so we can see through the shape. Set the line width to 1.0. Set the line join to be rounded. Then create the lineDashPattern. Next we create a path that matches the rectangle of our layer as to outline it correctly.

Finally we set the contents of the shape layer to a CGImageRef of the image we load from the bundle called balloon.jpg.

Once we connect the button in Interface Builder to the action called -toggleMarching, we can start and stop the marching ants animation.

Conclusion

The marching ants effect has limited use, but this example code demonstrates once again how Core Animation really simplifies a lot of visual tasks that would have at one time taken a lot more code to achieve. If there is anything I’ve learned while researching and writing the book it’s that Core Animation should be the go-to technology for a vast majority of visualizations on OS X and the iPhone. I find many questions related to the visual aspects of programming for both platforms on Stack Overflow have a very elegant answer in Core Animation. Other solutions exist, but they are often much more cumbersome. Until next time.

Marching Ants Demo Project

Comments

schwa says:

You can see the same effect here:

http://www.flickr.com/photos/jwight/3826876252/

And the code for it (from my MOOF project) is online here:

http://bitbucket.org/schwa/moof/src/tip/Source/CGameView/CGameMapView.m (grep for highlightLayerForLayer:)

andrewzboard says:

I tried reimplementing this as a desktop (i.e. non-Touch) app, and am not quite there. I made the obvious changes, some of which I’ll note here. But a pointer to any gotchas I have missed will be welcome.

viewDidLoad -> awakeFromNib
UIWindow -> NSWindow
[[UIColor clearColor] CGColor] -> CGColorGetConstantColor(kCGColorClear)

I instantiated a controller, but did it in the MainMenu.xib file rather than creating a new .xib file, hope that’s OK. Wasn’t sure how to rewrite the code in applicationDidFinishLaunching to be appropriate to AppKit. Suggestions?

Matt Long says:

@andrewzboard I’m not sure exactly what issues you are having without seeing code, but I can tell you on the desktop if you want to work with layers, you have to make sure your view is layer backed by calling [view setWantsLayer:YES] on it. Look at the earlier comment by John Wight and what he’s linked to. The video he has shows this working on an OS X app.

-Matt

andrewzboard says:

Thanks, setWantsLayer is at the least necessary if not sufficient. I’ll look more closely at his implementation.

andrewzboard says:

BTW, I ordered a copy of your book. On Amazon, it also suggested “You might also like…” and listed my own book! (Obj-C Pocket Ref.) Ah, little pleasures…

andrewzboard says:

Ah, my ants are marching now! This is going to be fun to play with. Many thanks. My eyes have been enlightened by this.

andrewzboard says:

Matt, I have a question now. I am loving this retro look of having marching ants walk around newly-created objects in my app… but what I find is that in order for the ants’ path to align perfectly with the new object (a button in this case), the code has to read as follows:

[shapeLayer setBounds:CGRectMake(0, 0, 0, 0)];

Somehow this wasn’t intuitive; I had previously set the bounds to be the same as those of the button, or the content view, or this and that. Of course, none of those worked. Why the empty or null bounds?

Matt Long says:

@andrewzboard

I’m actually not sure why the would be. I would think you would need to specify it explicitly. I’m wondering if there has been an update to the way CA layer bounds work in more recent versions as I don’t recall anything in the docs when I was researching writing the CA book. Will poke around when I have a few minutes and see if I can figure it out.

-Matt

andrewzboard says:

Just got the book. Nice work, dude! I am going camping this weekend and (nerd that I am) I am taking the book. :)

Matt Long says:

@andrewzboard

At least you’re not taking your laptop. ;-) Hope you enjoy the book.

-Matt

andrewzboard says:

Ha! That’s a presumption.. but in this case correct.