Premature Completion: An Embarrassing Problem

Working on a project recently, Jerry and I came across an odd bug. We have a two-level UI that allows the user to navigate between several different scroll views. For the sake of keeping things pretty, we want to reset a given scroll view before going back to the navigation interface paradigm. No problem, right? Something like this should do the trick:

[UIView animateWithDuration:2.f animations:^{
		[scrollView scrollRectToVisible:CGRectMake(0, 0, 10, 10) animated:YES];
	}completion:^(BOOL finished){
		[self.delegate resumeNavigation];
	}];

Turns out, nope! Somehow this runs the completion block in parallel with the animation block. The solution? Simply changing the YES to a NO in the scrollRectToVisible call.

But Why?

It seems that Core Animation (and therefore UIView animation) does some unexpected, not entirely welcome magic behind the scenes. If there is nothing to do in the animation block, the framework shrugs and runs the completion block immediately, rather than waiting the specified duration.

My guess, and this is just a guess, is that internally Core Animation depends on the animationDidStop:finished: delegate callback from a CAAnimationGroup or similar it sets up to handle everything that happens inside an animation block. When that callback fires, CA knows it’s time to kick off the completion block. If there are no animations created, there is nothing to send a callback. Rather than set a timer to wait for nothing to happen, the completion block runs right away, because why not?

This is seductive reasoning, and has the advantage of being easy to code. Unfortunately, it’s not always what the user of the API expects. (I would venture to say never!) In our case, it means the naive code fails because (again, guessing) asking the scroll view to animate its scrolling sets up another animation context. Asking it not to animate, by contrast, allows Core Animation to create an implicit animation and work its magic.

What’s the Solution?

Unfortunately, for us API clients there really isn’t one. This is more of an informational post than anything: once you’re aware this can be a problem, you may save yourself hours you otherwise would’ve spent in fruitless debugging. Believe me, I’ve been there on this issue.

The best thing you can do is play around and see where exactly unexpected things happen.  As long as you keep the “are there any animations created?” question in mind, whatever behavior you see will be easy to explain. But reasoning a priori and figuring out what to expect is impossible without inside knowledge of the implementation of UIKit. Which brings us to…

Toys

To see just what was going on, I put together a tiny sample project. You can find it here. It has a UIProgressView and a UITextView, both set up to perform some transition either animated or not. The important part of the code looks like this:

- (void)party:(BOOL)animated
{
	[UIView animateWithDuration:2.f animations:^{
		[self.partyProgress setProgress:1 animated:animated];
	}completion:^(BOOL finished){
		self.partyLabel.text = @"PARTY TIME!";
	}];
}

- (void)study:(BOOL)animated
{
	[UIView animateWithDuration:2.f animations:^{
		[self.studyView scrollRectToVisible:CGRectMake(0, 0, 10, 10) animated:animated];
	}completion:^(BOOL finished){
		self.studyLabel.text = @"STUDY TIME!";
	}];
}

Try scrolling the text view all the way to the bottom, then compare the difference between animating and not animating inside of an animation block. Then do the same with the progress view. Why would they be different?

There are plenty of other classes in UIKit with methods that take an animated: argument. I encourage you to plug them into this test app to see just what happens. Protect yourself from being surprised next time. Take any precaution you can against the embarrassment and social shunning that inevitably accompany premature completion.

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.