16

I have a series of 8 UIView animations that occur directly after my view is loaded. Right now, I am accomplishing this by using the animationDidStop:finished:context delegate method, and everything works as expected. The problem is that I have a new method for each animation. Most of the code in each of these methods is repeated, with only the animation duration and the actual positioning of elements changing.

I tried to create a single method which would be called and have the context hold the parameters needed to change the UI appropriately, but it seems to be recursively calling itself well past the amount of times I call it:

-(void)animationDidStop:(NSString *)animationID finished:(BOOL)finished context:(void *)context{
    NSNumber *number = (NSNumber *)context;
    int animationStep = [number intValue];
    int nextAnimationStep = animationStep + 1;
    NSNumber *nextAnimationStepNumber = [NSNumber numberWithInt:nextAnimationStep];
    NSLog(@"Animation Step: %i", animationStep);

    CGRect firstFrame = CGRectMake(self.feedsScroll.frame.size.width * 2, 0.0f, self.secondFeedView.view.frame.size.width, self.secondFeedView.view.frame.size.height);
    CGRect thirdFrame = CGRectMake(self.feedsScroll.frame.size.width * 2, 0.0f, self.thirdFeedView.view.frame.size.width, self.thirdFeedView.view.frame.size.height);

    [UIView beginAnimations:nil context:nextAnimationStepNumber];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    [UIView setAnimationDelegate:self];

    if (animationStep < 8) 
        [UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];

    NSLog(@"Beginning animations");

    switch (animationStep) {
        case 0:
            [UIView setAnimationDuration:.3];
            self.firstFeedView.view.center = CGPointMake(self.firstFeedView.view.center.x + 30, self.firstFeedView.view.center.y);
            break;
        case 1:
            [UIView setAnimationDuration:.3];
            self.firstFeedView.view.center = CGPointMake(self.firstFeedView.view.center.x  - 30, self.firstFeedView.view.center.y);
            break;
        case 2:
            [UIView setAnimationDuration:.3];
            [self.secondFeedView.view setFrame:firstFrame];
            break;
        case 3:
            [UIView setAnimationDuration:.3];
            self.secondFeedView.view.center = CGPointMake(self.secondFeedView.view.center.x + 30, self.firstFeedView.view.center.y);
            break;
        case 4:
            [UIView setAnimationDuration:.3];
            self.secondFeedView.view.center = CGPointMake(self.secondFeedView.view.center.x - 30, self.firstFeedView.view.center.y);
            break;
        case 5:
            NSLog(@"Animation step 6");
            [UIView setAnimationDuration:.5];
            self.firstFeedView.view.center = CGPointMake(self.firstFeedView.view.center.x - 230, self.firstFeedView.view.center.y);
            self.secondFeedView.view.center = CGPointMake(self.secondFeedView.view.center.x - 230, self.firstFeedView.view.center.y);
            break;
        case 6:
            [UIView setAnimationDuration:.5];
            [self.thirdFeedView.view setFrame:thirdFrame];
            break;
        case 7:
            [UIView setAnimationDuration:.3];
            self.thirdFeedView.view.center = CGPointMake(self.thirdFeedView.view.center.x + 30, self.firstFeedView.view.center.y);
            break;
        case 8:
            [UIView setAnimationDuration:.3];
            self.thirdFeedView.view.center = CGPointMake(self.thirdFeedView.view.center.x - 30, self.thirdFeedView.view.center.y);
            break;
        default:
            break;
    }

    [UIView commitAnimations];  
}

I know this is probably a naive implementation. I am new to iPhone development, and am looking for some best practices to apply here. Am I going about this in the wrong way?

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
Mark Struzinski
  • 32,945
  • 35
  • 107
  • 137

5 Answers5

19

If you're willing to step up to iOS 4.0, the new blocks-based animation approach can make this animation chaining trivial. For example:

[UIView animateWithDuration:1.0 animations:^{ view.position = CGPointMake(0.0f, 0.0f); } completion:^(BOOL finished){
    [UIView animateWithDuration:0.2 animations:^{ view.alpha = 0.0; } completion:^(BOOL finished){
        [view removeFromSuperview]; }];
}]

will cause view to animate to (0, 0) over a duration of 1 second, fade out for 0.2 seconds, then be removed from its superview. These animations will be sequential.

The first block in the +animateWithDuration:animations:completion: class method contains the property changes to animate at once, and the second is the action to be performed on completion of the animation. Because this callback can contain another animation of this sort, you can nest them to create arbitrary chains of animations.

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • Great idea, but we're committed to supporting at least iOS 3.x for now. I wish we could, though. This would solve a lot of problems! – Mark Struzinski Oct 03 '10 at 22:45
16

You can use blocks for this purpose and get a very clean result.

NSMutableArray* animationBlocks = [NSMutableArray new];

typedef void(^animationBlock)(BOOL);

// getNextAnimation
// removes the first block in the queue and returns it
animationBlock (^getNextAnimation)() = ^{
    animationBlock block = (animationBlock)[animationBlocks firstObject];
    if (block){
        [animationBlocks removeObjectAtIndex:0];
        return block;
    }else{
         return ^(BOOL finished){};
    }
};

//add a block to our queue
[animationBlocks addObject:^(BOOL finished){;
    [UIView animateWithDuration:1.0 animations:^{
        //...animation code...
    } completion: getNextAnimation()];
}];

//add a block to our queue
[animationBlocks addObject:^(BOOL finished){;
    [UIView animateWithDuration:1.0 animations:^{
        //...animation code...
    } completion: getNextAnimation()];
}];

//add a block to our queue
[animationBlocks addObject:^(BOOL finished){;
    NSLog(@"Multi-step Animation Complete!");
}];

// execute the first block in the queue
getNextAnimation()(YES);

Taken from: http://xibxor.com/objective-c/uiview-animation-without-nested-hell/

Andrew Fuchs
  • 306
  • 2
  • 4
10

We created a component for chaining animation steps declaratively using blocks (CPAnimationSequence on Github). We described the motivations and the rationale in our iOS development blog.

It gives you very readable code, something like this:

[[CPAnimationSequence sequenceWithSteps:
    [CPAnimationStep           for:0.25 animate:^{ self.imageView.alpha = 0.0; }],
    [CPAnimationStep           for:0.25 animate:^{ self.headline.alpha = 0.0;  }],
    [CPAnimationStep           for:0.25 animate:^{ self.content.alpha = 0.0;   }],
    [CPAnimationStep after:1.0 for:0.25 animate:^{ self.headline.alpha = 1.0;  }],
    [CPAnimationStep           for:0.25 animate:^{ self.content.alpha = 1.0;   }],
    nil]
runAnimated:YES];
Yang Meyer
  • 5,409
  • 5
  • 39
  • 51
  • That looks like a nice elegant solution. (voted) I'll have to check out the github project. – Duncan C Apr 04 '13 at 00:26
  • Nice code. However, the blog asks for a username and password login, but I do not see any register link, can I read the blog somewhere else? – lxmfly123 May 23 '16 at 01:21
1

The "context" is a void*, and therefore not retained. There are a few options:

  • Retain it when you set it. Autorelease it at the start of the callback. This feels icky (Objective-C functions ought to retain/release appropriately, but unfortunately the context isn't an id).
  • Store the number in the string: int animationStep = [animationID intValue]; and [UIView beginAnimations:[NSString stringWithFormat:@"%d", nextAnimationStep] context:NULL];. This also feels icky.
  • Assuming that UIKit/CoreAnimation doesn't dereference the pointer, int animationStep = (int)context; and [UIView beginAnimations:nil context:(void*)nextAnimationStep];. This is icky (and probably causes undefined behaviour according to the C standard).

On another note, finished:(BOOL)finished should be finished:(NSNumber*)finished. They made a mistake in the original docs; it was presumably easier to change the docs to reflect the API instead of changing the API and having to magically maintain backwards compatibility (even though passing a bool is more sensible).

tc.
  • 33,468
  • 5
  • 78
  • 96
  • This is an old post. ARC actually gives you a solution to this problem (if you're still using the old style beginAnimations:context: ...commitAnimations method. You can use a __bridge_retained cast of your NSObject to a void* pointer, and then in your completion method, use __bridge_transfer. I needed to do this for some transition animations that used the old-style calls and it works. (The compiler complains, but the object gets retained and released properly.) – Duncan C Apr 04 '13 at 00:24
-1
 // create the view that will execute our animation
 UIImageView* logo = [[UIImageView alloc] initWithFrame:self.view.frame];
 // load all the frames of our animation
 logo.animationImages = [NSArray arrayWithObjects:  
                                 [UIImage imageNamed:@"frame_00001.png"],

                         [UIImage imageNamed:@"frame_00002.png"],
                         [UIImage imageNamed:@"frame_00003.png"],
                         [UIImage imageNamed:@"frame_00004.png"],
                                 [UIImage imageNamed:@"frame_00005"], nil];

 // all frames will execute in 3 seconds
 logo.animationDuration =3.3;
 // repeat the annimation forever
logo.animationRepeatCount = 1;
 // start animating
 [logo startAnimating];
 // add the animation view to the main window 
 [self.view addSubview:logo];
 [logo release]; 

put this in the ViewDidLoad

Bobj-C
  • 5,276
  • 9
  • 47
  • 83
  • Each view animation has a different behavior in my case. Basically, one slides in, the second one slides in and bounces the first out of the way, then the third slides slightly into view on the right. The code you show above performs the same animation on all views, right? – Mark Struzinski Oct 03 '10 at 13:39