3
void (^first_animation)();
void (^second_animation)(BOOL finished);


// First animation

first_animation = ^()
{
    g_pin_info_screen.view.alpha = 1.0;
};


// Second animation

second_animation = ^(BOOL finished)
{
    g_shadow_layer.opacity = 0.0;

    void (^set_opacity_to_1)();

    set_opacity_to_1 = ^()
    {
        g_shadow_layer.opacity = 1.0;
    };

    [UIView animateWithDuration : 2.0
            delay               : 0.0
            options             : UIViewAnimationCurveEaseInOut
            animations          : set_opacity_to_1
            completion          : nil
     ];

};



// Begin the animations

{

    float duration;

    duration = 0.35;

    [UIView animateWithDuration : duration
            delay               : 0.00
            options             : UIViewAnimationCurveEaseInOut
            animations          : first_animation
            completion          : second_animation
    ];

}

First animation executes as expected. But second animation completes but without any animation.

Hope somebody could comment on whether the above scheme is the proper way of doing this or not.

Stanley
  • 4,446
  • 7
  • 30
  • 48

5 Answers5

13
__block NSMutableArray* animationBlocks = [NSMutableArray new];
typedef void(^animationBlock)(BOOL);

// getNextAnimation
// removes the first block in the queue and returns it
animationBlock (^getNextAnimation)() = ^{

    if ([animationBlocks count] > 0){
        animationBlock block = (animationBlock)[animationBlocks objectAtIndex:0];
        [animationBlocks removeObjectAtIndex:0];
        return block;
    } else {
        return ^(BOOL finished){
            animationBlocks = nil;
        };
    }
};

[animationBlocks addObject:^(BOOL finished){
    [UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
        //my first set of animations
    } completion: getNextAnimation()];
}];


[animationBlocks addObject:^(BOOL finished){
    [UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
       //second set of animations
    } completion: getNextAnimation()];
}];



[animationBlocks addObject:^(BOOL finished){
    [UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
        //third set
    } completion: getNextAnimation()];
}];


[animationBlocks addObject:^(BOOL finished){
    [UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
        //last set of animations
    } completion:getNextAnimation()];
}];

// execute the first block in the queue
getNextAnimation()(YES);   
JeffRegan
  • 1,322
  • 9
  • 25
  • Thanks for the answer. It's a bit more involved than I'd in mind. But it's very sophisticated. Have you tried it in your project ? – Stanley Jul 30 '13 at 20:33
  • It works well and is cleaner for a lot of chained animations. If you try to link a bunch of animations in the completion block of a UIView animation block, it can get ugly real quick. However, if only only have a couple animations, this is a bit overkill. – JeffRegan Jul 30 '13 at 20:33
  • Basically, this creates a queue of animations and fires them off one after another. UIViewAnimationOptionCurveLinear makes sure that the animations are smooth. – JeffRegan Jul 30 '13 at 20:35
  • I see what you mean. It's nice to have chained animations. But it may not be easy to implement them elegantly. – Stanley Jul 30 '13 at 20:36
  • Note that there's a retain cycle here: getNextAnimation -> animationBlocks -> [one of the animation blocks] -> getNextAnimation – Chris Devereux Aug 02 '13 at 18:23
  • @Stanley I've put another approach in an answer which uses a third party library. It may look more "intuitive" - and more "synchronous" (but it's 100% asynchronous). The third party library is free to use and on GitHub. – CouchDeveloper Aug 02 '13 at 18:59
  • @ChrisDevereux I had to do some research for what a retain cycle was. Found a pretty good explanation: http://www.quora.com/What-is-a-retain-cycle. While running instruments, I didn't get any leaks for this animation. Any advice? – JeffRegan Aug 02 '13 at 19:47
  • 2
    @JeffCompton Making `animationBlocks` a `__block` type variable and then niling it out after the last animation would do the trick (assuming the sequence always completes). Unfortunately, doing anything recursive with blocks requires some explicit memory management (and is therefore error-prone) in Obj-C, due to the cycles issue. Any strong references captured by a block are retained by the block. – Chris Devereux Aug 02 '13 at 22:38
  • 1
    (also, leaks is fairly conservative. I've never seen it report a false positive but it often misses cycles) – Chris Devereux Aug 02 '13 at 22:42
  • @ChrisDevereux I edited my post to fix the retain cycle. Thanks for the lesson. – JeffRegan Aug 03 '13 at 04:08
2

With the help of the third party library there is a solution that looks like as below:

First, for convenience, define a category for UIView like so:

+(RXPromise*) rx_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations 
{
    RXPromise* promise = [RXPromise new];
    [UIView animateWithDuration:duration animations:animations: ^(BOOL finished){
         // ignore param finished here
         [promise fulfillWithValue:@"finished"]; // return just a string indicating success
    }];    
    return promise;
}

Then, define any number of asynchronous animations which execute one after the other, as follows:

[UIView rx_animateWithDuration:duration animation:^{
        ... //define first animation
    }]
.then(^id(id result){
    // ignore result, it contains the fulfill value of the promise, which is @"finished"
    return [UIView rx_animateWithDuration:duration animation:^{
        ... // define second animation
    }];
}, nil)
.then(^id(id result){
    return [UIView rx_animateWithDuration:duration animation:^{
        ...  // define third animation
    }];
}, nil)
.then(^id(id result){
    return [UIView rx_animateWithDuration:duration animation:^{
        ... // and so force
    };
}, nil);

The above statement is asynchronous!

With one line additional code you can achieve cancellation:

RXPromise* rootPromise = [UIView rx_animateWithDuration:duration animation:^{
        ... //define first animation
    }];

rootPromise.then(^id(id result){
    return [UIView rx_animateWithDuration:duration animation:^{
        ... // define second animation
    }];
}, nil)
.then(^id(id result){
    return [UIView rx_animateWithDuration:duration animation:^{
        ...  // define third animation
    }];
}, nil)
...

// later, in case you need to cancel pending animations:
[rootPromise cancel];

"RXPromise" library is available on GitHub: RXPromise. It's specifically designed for these use cases, and more. Due to full disclosure: I'm the author ;)

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • This almost work, since we are talking about animation you need to call thenOnMain instead of just then. – Shay Erlichmen Apr 12 '14 at 13:49
  • PS it would be nice to be able to generate a promise that will be called on main no matter if then or thenOnMain is called. – Shay Erlichmen Apr 12 '14 at 13:50
  • @ShayErlichmen I assumed, that `animateWithDuration:animations:` *can* be invoked from a non-main thread, too. If not, than yes - we should use `thenOnMain` ;) – CouchDeveloper Apr 12 '14 at 16:59
  • Very cool solution! I had implemented something similar with AnyPromise: `+(AnyPromise *) sm_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations { return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { [UIView animateWithDuration:duration animations:animations completion:^(BOOL finished){ resolve(nil); }]; }]; }` – smileham Jul 29 '16 at 22:55
  • I ended up using UIView animateWithDuration:delay... because that was easier though (can have overlapping animations). – smileham Jul 29 '16 at 22:59
1

Just check here: https://gist.github.com/vadimsmirnovnsk/bce345ab81a1cea25a38

You can chain it in functional style:

dispatch_block_t animationsBlock = ^{
    [self.view updateConstraintsIfNeeded];
    [self.view layoutIfNeeded];
};

[[[[[[[[[BARAnimation construct]
    initially:animationsBlock]
    animationWithDuration:0.425 animationConditions:^{
        [gridView mas_updateConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(imageView).with.offset(32.0);
        }];
    } animations:animationsBlock]
    animationWithDuration:0.425 animationConditions:^{
        [gridView mas_updateConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(imageView).with.offset(0.0);
        }];
    } animations:animationsBlock]
    animationWithDuration:0.425 animationConditions:^{
        [gridView mas_updateConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(imageView).with.offset(-32.0);
        }];
    } animations:animationsBlock]
    animationWithDuration:0.425 animationConditions:^{
        [gridView mas_updateConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(imageView).with.offset(0.0);
        }];
    } animations:animationsBlock]
    animationWithDuration:0.8 animationConditions:nil animations:^{
        foreView.alpha = 1.0;
    }]
    finally:^{
        [self.didEndSubject sendNext:[RACUnit defaultUnit]];
        [self.didEndSubject sendCompleted];
    }]
    run];
0

You need to chain them together by using + (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion

Within the options: argument, you need to include UIViewAnimationOptionBeginFromCurrentState

Good luck!

SushiGrass Jacob
  • 19,425
  • 1
  • 25
  • 39
  • Have changed the option of "show_shadow" to : UIViewAnimationCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState. But the result is the same. Anymore hints ? – Stanley Jul 30 '13 at 19:44
  • This is unclear, this does not explain how to chain. two differnet animations. – AlexWien Jul 30 '13 at 19:58
  • In the block "Begin Animations" I have put the second animation "show_shadow" in the "completion" argument. – Stanley Jul 30 '13 at 20:11
0

In the completion handler of the first animation, start the second one.

AlexWien
  • 28,470
  • 6
  • 53
  • 83
  • I think I did what you suggested in the block labeled "Begin the animations". Please comment if it is actually what you'd suggested. – Stanley Jul 30 '13 at 20:15
  • yes, but why is animation duration 0.00, it should be a higher value. Further the Quart2d gudie dewcribes chaining animations via completion handler, too. – AlexWien Jul 30 '13 at 22:47
  • Guess you have misread the coding, it is "delay" that was set to 0.00. Duration was set to 0.35 via a variable. But everything seems to be ok now. If my most recent testing is right, the problem is due to the CALayer. If an UIView were used everything should be ok. – Stanley Jul 31 '13 at 04:34
  • You are right, It was th eanswer of Jeff who has the 0.00 animation duration. – AlexWien Jul 31 '13 at 12:49
  • @AlexWien Ya, I had to update some invisible elements based on prior animations. I didn't want these animations to take time because they aren't visible. I'll update it so it's less confusing. – JeffRegan Aug 02 '13 at 18:06