iPhone Animation Sequence

When writing Postage and WORD SPIN we encountered the same problem again and again: the need to chain a sequence of animations together. For example, when the user navigates to the next page we first animate away the controls for the current view, switch views, then animate the controls in for the new view. Initially, we set the UIView delegate and then in our specified selector implementation kicked off the next animation which itself would eventually call back into a completion selector, and so on. But it’s largely the same code over and over, and you either need a bunch of similar looking animationDidStop implementations, or a single implementation that branches based on the animation identifier. For example:

Old Way

 (void) doMoveFromView1ToView2Animation
{
[app beginIgnoringInteractionEvents];[UIView beginAnimations: @“HideView1Animation” context: foo];
[UIView setAnimationDuration: kDuration];
[UIView setAnimationDelegate: self];
[UIView setAnimationDidStopSelector:
@selector(hideView1AnimationDidStop:finished:context:)];[self hideView1Code];[UIView commitAnimations];
} (void)hideView1AnimationDidStop:(NSString*)animationID
finished:(NSNumber*)finished
context:(void*)context
{
[UIView beginAnimations: @“SwapView1AndView2Animation” context: foo];
[UIView setAnimationDuration: kDuration];
[UIView setAnimationDelegate: self];
[UIView setAnimationDidStopSelector:
@selector(swapView1AndView2AnimationDidStop:finished:context:)];[self swapView1AndView2Code];[UIView commitAnimations];
}

 (void)swapView1AndView2AnimationDidStop:(NSString*)animationID
finished:(NSNumber*)finished
context:(void*)context
{
[UIView beginAnimations: @“ShowView2Animation” context: foo];
[UIView setAnimationDuration: kDuration];
[UIView setAnimationDelegate: self];
[UIView setAnimationDidStopSelector:
@selector(showView2AnimationDidStop:finished:context:)];

[self showView2Code];

[UIView commitAnimations];
}

 (void)showView2AnimationDidStop:(NSString*)animationID
finished:(NSNumber*)finished
context:(void*)context
{
[app endIgnoringInteractionEvents];

[self doStuff];
}


When we embarked on WORD SPIN it was time to build something to help tackle this problem. I settled on the concept of sequences, where each individual animation is a step in the overall animation sequence. For the user example of moving from one page to the next in Postage, the new code looks like this:

New Way

 (void) doMoveFromView1ToView2Animation
{
NSMutableArray* steps = [NSMutableArray array];[steps addObject: [RSViewAnimationSequenceStep stepWithTarget: self
selector: @selector(hideView1Code)
duration: kDuration]];
[steps addObject: [RSViewAnimationSequenceStep stepWithTarget: self
selector: @selector(swapView1AndView2Code)
duration: kDuration]];
[steps addObject: [RSViewAnimationSequenceStep stepWithTarget: self
selector: @selector(showView2Code)
duration: kDuration]];[[RSSequenceManager sharedManager] invokeSequence: steps];
}

What I really like about this approach is that the entire animation sequence is specified in the same location and it’s clear how many independent steps there are up front, rather than jumping from one place in the code to the next to see what the whole animation will do. Granted, you could almost achieve the same visual effect by refactoring all the “beginAnimation” code into a reusable piece. In fact, that’s how I started.

The fundamental piece of this paradigm is the sequence step. RSViewAnimationSequenceStep inherits from RSSequenceStep, a base class to define the invocation and delegation of the step. RSViewAnimationSequenceStep is primarily a refactoring of the beginAnimation and commitAnimation boiler plate (I’ve removed the definition of the helper, init, and dealloc selectors for brevity):

RSViewAnimationSequenceStep

@interface RSViewAnimationSequenceStep : RSSequenceStep
{
id      target_;
SEL     selector_;
float   duration_;
}+ (id) stepWithTarget: (id) target selector: (SEL) selector duration: (float) duration; (void) invoke;@end@implementation RSViewAnimationSequenceStep// helper, init, and dealloc removed for brevity (void) invoke
{
[UIView beginAnimations: @“RSViewAnimationSequenceStep” context: nil];
[UIView setAnimationDuration: duration_];
[UIView setAnimationDelegate: self];
[UIView setAnimationDidStopSelector:
@selector(animationDidStop:finished:context:)];

[target_ performSelector: selector_];

[UIView commitAnimations];
}

 (void) animationDidStop:(NSString *)animationID
finished:(NSNumber *)finished
context:(void *)context
{
// delegate is defined in the base class RSSequenceStep (see below)
[delegate_ stepCompleted: self];
}

@end


The RSSequenceStep is a simple base class to define the general interface:

RSSequenceStep

@interface RSSequenceStep : NSObject
{
id  delegate_;
}@property (nonatomic, assign)   id  delegate;
// The object that implements stepCompleted. (void) invoke;
// Called to invoke the code necessary for the step. @end@interface NSObject (RSSequenceStepDelegate) (void) stepCompleted: (RSSequenceStep*) step;@end


RSViewAnimationSequenceStep encapsulates handling the beginning and ending of an animation, and then calls back to a delegate when complete. It is a self contained item that can be used independently. If so desired, we could stop here and have client code declare the RSViewAnimationSequenceStep, set the delegate to self, and achieve a large part of the refactoring. But the steps would still be spread apart in the code. That’s where the RSSequenceManager, and it’s implementation detail class RSSequence, comes into play, as those two handle chaining the steps together to encapsulate the entire animation.

RSSequenceManager

@implementation RSSequenceManager

// things removed for brevity

 (void) invokeSequence: (NSArray*) steps
{
RSSequence* sequence = [RSSequence sequenceWithSteps: steps delegate: self];

[sequences_ addObject: sequence];

if ( [sequences_ count] == 1 )
{
[sequence invoke];
}
}

 (void) sequenceCompleted: (RSSequence*) sequence
{
[sequences_ removeObject: sequence];

if ( [sequences_ count] > 0 )
{
[[sequences_ objectAtIndex: 0] invoke];
}
}
@end


The bulk of the work, however, happens inside the RSSequence class:

RSSequence

@interface RSSequence : NSObject
{
int         currentStepIndex_;
NSArray*    steps_;
id          delegate_;
} (id) initWithSteps: (NSArray*) steps delegate: (id) delegate; (void) invoke;
// Called to initiate the sequence. @end@interface NSObject (RSSequenceDelegate) (void) sequenceCompleted: (RSSequence*) sequence;@end

@implementation RSSequence

 (id) initWithSteps: (NSArray*) steps delegate: (id) delegate;
{
self = [super init];

if ( self )
{
steps_ = [steps retain];

for ( RSSequenceStep* step in steps_ )
{
step.delegate = self;
}
}

return self;
}

 (void) dealloc
{
[steps_ release];
[super dealloc];
}

 (void) invoke
{
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];

currentStepIndex_ = 0;

[self invokeNextStep];
}

 (void) invokeNextStep
{
if ( currentStepIndex_ < [steps_ count] )
{
RSSequenceStep* step = [steps_ objectAtIndex: currentStepIndex_++];

[step invoke];
}
else
{
[[UIApplication sharedApplication] endIgnoringInteractionEvents];

[delegate_ sequenceCompleted: self];
}
}

 (void) stepCompleted: (RSSequenceStep*) step
{
[self invokeNextStep];
}

@end


The sequence manager doesn’t know or care that a sequence is an animation, and a sequence also doesn’t know or care what an individual sequence step does. Those classes only assume that a sequence step’s ending can, but doesn’t have to, come later than its beginning. Given that, we’ve also implemented other sequence step subclasses: one that is a simple selector call, one that encapsulates multiple simultaneous steps, and one that handles openGL animations. I’ve pared the code down a fair bit for this posting, and left out detail — for example, our view animation step handles delays, custom timing functions, and transitions. But the basic idea is all there.

Lastly, Joe Conway over at the Big Nerd Ranch posted a blog about this problem, and described a solution using a subclass of CALayer that calls completion targets for a given animation key. I like his approach because it theoretically handles all the different kinds of animations, and I’ll be thinking about how to work his idea into our sequences.