CALayer’s Parallel Universe

Ever tried to animate a UIView’s position?  It’s easy, using UIView animation class methods like animateWithDuration:animations: and friends.  Simply change the position inside the “animations” block, et voila, a pretty animation with the duration of your choice.

But have you ever tried to change that animation while it’s running?  Suppose you’re writing a simple application that animates a view to a point the user touches. The first one works fine, but after that, additional animation blocks will result in the view animating from the previous final location that it was supposed to go to — not from the location it’s very clearly occupying onscreen.

“Why Is That?”*

Now, I’m fudging a little bit for simplicity’s sake. One of the more complicated UIView animation methods allows options, one of which is UIViewAnimationOptionBeginFromCurrentState.  That fixes this problem in one fell swoop — as the name implies, animations will begin from the view’s current state, that being its position onscreen in our example, rather than the final state. But the larger question remains: why is such an option necessary at all? What’s going on behind the scenes? Join me as I draw back the curtain.

The Great Cover-Up

You may have heard somewhere along the way that UIViews are “backed” by CALayers. In practical terms, this means that CALayers actually handle the rendering, compositing, and animation of your view.  The UIView class is a relatively complex add-on that knows about things like user interaction (in the form of gesture recognizers), printing, and all the specialized things that UIView subclasses do.  But many of the core UIView methods — things that deal with view hierarchy, display, colors, even hit testing — simply call through to the underlying CALayer, which has similar methods.

Now that we know CALayer is doing all the work under the covers (which are themselves behind a curtain, as you’ll recall), we can talk about exactly how it does its dirty deeds.  What really happens when a view (that is, a layer) animates from point A to B?

The Parallel Universe

It’s not magic!  It’s even better: technology.  Two extraordinarily divergent things happen when you kick off an animation.  The animated property of the view (in our example, position) doesn’t animate at all!  It is immediately set to the final value. If you start an animation with a duration of a year, and in the next line of code read back the view’s position, you’ll get the position you’d expect the view to occupy a year from now. But that makes no sense — the view is onscreen, and it’s clearly not all the way over there. It doesn’t yet appear to have moved at all.  The view seems to be expressing two contradictory pieces of state.

(By the way, the fact that the view’s position jumps to the final value as soon as the animation begins is why the first example I talked about doesn’t work. When you start an animation, it’s internally expressed as “animate from A to B”, and the “A” is implicitly set to be the view’s current position. So when you animate from A to B, and then change it to C halfway through, the view already considers itself to be at B, although it does not appear so to the naked eye. But I suspect you may be more interested in the underlying question at this point! Let’s continue.)

If the view’s position changes instantaneously, but we can watch it travel across the screen, there must be some kind of trick taking place. And indeed, there is an incredibly pervasive trick. The secret is this: the view your code talks to is not the view on screen at all.  Indeed, no UI element you address is ever on screen! Instead, Core Animation creates a parallel view hierarchy, from UIWindow on down. What you see on screen is something like your view’s evil twin.

Did I blow your mind?

Theory

What Core Animation is doing is a low-level Model/View separation, just like the MVC pattern with which you’re familiar.  Wait, isn’t everything we’re talking about a view? Yes, we’re overloading the term here. Now we’re talking about model data about an object that happens to be a UIView, and the view of that model data. The model is the UIView you talk to — it contains the truth about the data (the position of the UIView).  The view is the parallel CALayer on screen — it’s a visual representation of the data. It can animate rather than moving immediately because just as in other MVC situations, the view renders the data however it feels appropriate; it’s not guaranteed to be a one-to-one representation.

This is cool to know, but it’s only of academic interest if you can’t access the parallel view hierarchy. Fortunately, you can! Not on the UIView level, but CALayer’s presentationLayer method gets you there. Terminology time: A layer’s “presentation layer” is the view I was talking about before. To move back and forth between the hierarchies, presentation layers have a “model layer” (accessed through the modelLayer method) that is, as you’d guess, the model — the layer you usually use in your code. Using these two methods, you can jump between the model and view layer hierarchies with ease.

Code

The practical upshot of this: the data of the presentation layer will reflect where things currently are on screen, as opposed to the model layer we’re used to. Suddenly, animating from a view’s current position is simple (although you will have to drop down into Core Animation to do it).  As a refresher, here’s the pertinent part of the example I started with. Remember, the idea here was to animate a view to the user’s touch, but it doesn’t animate cleanly once there’s another animation in effect.

- (void)viewDidLoad
{
	[super viewDidLoad];
	touchView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
	touchView.backgroundColor = [UIColor redColor];
	[self.view addSubview:touchView];
	UITapGestureRecognizer *gr = [[UITapGestureRecognizer alloc] 
		initWithTarget:self action:@selector(tap:)];
	[self.view addGestureRecognizer:gr];
}

- (void)tap:(UITapGestureRecognizer*)gr
{
	[UIView animateWithDuration:1.f animations:
	 ^{touchView.center = [gr locationInView:self.view];}];
}

And here are the changes we have to make to use the presentation layer to run the animation from the current location:

- (void)tap:(UITapGestureRecognizer*)gr
{
	CGPoint newPos = [gr locationInView:self.view];
	CGPoint oldPos = [touchView.layer.presentationLayer position];
	CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
	animation.fromValue = [NSValue valueWithCGPoint:oldPos];
	animation.toValue = [NSValue valueWithCGPoint:newPos];
	touchView.layer.position = newPos;
	[touchView.layer addAnimation:animation forKey:@""];
}

What’s this all about? Just as before, we’re getting the gesture recognizer’s location to determine where we want to animate to. But where before we were depending on the UIView animation method to tell Core Animation to create an implicit animation, now we create our own, aptly called an explicit animation. (More posts on this distinction to come: for now, all that matters is that usually Core Animation will do the right thing for you. That’s an implicit animation.) The basic animation here simply takes a “from” and a “to”, which we fill in appropriately. (The fact that we have to wrap the CGPoints in NSValues is an unfortunate implementation detail.) We then set the final value, and right after, add the animation. It looks like a lot more code than before, but that’s really all that’s necessary, and this methodology can be used to do much more complex stuff than UIView animations are capable of. Check out the CAAnimation subclasses to see how you can do keyframe animations and lots more.

Problem Solved!

And a whole lot more besides. More on all of these concepts to come!

Joel Kraut

About Joel Kraut

Developing on Apple platforms for, holy shit, over ten years now. Find me on linkedin and twitter. My personal website is foon.us.
This entry was posted in Explanation and tagged , , , . Bookmark the permalink.

One Response to CALayer’s Parallel Universe

  1. Pingback: Premature Completion: An Embarrassing Problem | ultrajoke

Leave a Reply

Your email address will not be published. Required fields are marked *