简体   繁体   中英

how to invoke a single completion block for a nested group of UIView animateWithDuration calls?

I have a batch of animation calls, invoked by iterating through an array. All these calls are nested within an encapsulating animation block so that they execute effectively in parallel. I also have a completion block with I only want to fire once all of the nested animations have completed.
The problem is that the nested animations are of unknown durations, so I cannot simply calculate which call will be the last to finish and set the completion block on this call. Similarly I cannot calculate the duration and use a delayed invocation on the completion block.
Hopefully an example will make this clearer. This is a (very simplified) version of what I'm trying to do:

-(void) animateStuff:(CGFloat)animationDuration withCompletionBlock:(void) (^)(BOOL)completionBlock {

// encapsulating animation block so that all nested animations start at the same time
 [UIView animateWithDuration:animationDuration animations:^{

    for(MyObject* object in self.array) {
        // this method contains other [UIView animateWithDuration calls...
        [self animationOfUnknownDurationWithObject:object nestedCompletionBlock:^(BOOL finished) {
            // what I effectively what here is:
            if(last animation to finish) {
                completionBlock(YES);
            }
        }];
    }

 }]; // cannot use the completion block for the encapsulating animation block, as it is called straight away
}

The functionality provided by using a dispatch_groups and asynchronous invocation as described here:
http://developer.apple.com/library/ios/#documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html

would obviously be ideal, however the UIView animateWithDuration call is an asynchronous call in itself and so invoking it within dispatch_group_async will not work properly.
I know I could do something like having a __block count variable that gets decremented within nestedCompletionBlock as a way of determining which is the final one, but in the code that I have this is pretty messy (the above is a simplified example).
Is there a good way of doing this? Perhaps some way of doing animateWithDuration synchronously, so that it will work with dispatch_group_async?

Working on iOS 5.0.

UPDATE:

@Rob-
Thankyou for your answer! This nearly solves my problem - hadn't looked into CATransaction before, guess I should have dug a little deeper before asking. :)
However one issue remains. As I mentioned before, my example was simplified, and the remaining problem arises from the fact that the animations have nested completion blocks (ie. for chained animations) which I need to include within the enclosing transaction.
So for example if I run the following code:

-(void) testAnim {
NSArray* testArray = [NSArray arrayWithObjects:self.redView, self.blueView, nil];

[CATransaction begin]; {
    [CATransaction setCompletionBlock:^{
        NSLog(@"All animations complete!");
    }];

    for(UIView* view in testArray) {
        [CATransaction begin]; {

            [CATransaction setCompletionBlock:^{

                [CATransaction begin]; {
                    [CATransaction setCompletionBlock:^{
                        NSLog(@"2nd stage complete");
                    }];

                    NSLog(@"Animation 2nd stage");
                    [UIView animateWithDuration:2 animations:^{
                        setX(view, 100);
                    }];
                } [CATransaction commit];
            }];

            NSLog(@"animation 1st stage");
            [UIView animateWithDuration:2 animations:^{
                setX(view, 150);
            }];
        }[CATransaction commit];
    }

} [CATransaction commit];
}

I get the following output:

2011-12-08 15:11:35.828 testProj[51550:f803] animation 1st stage
2011-12-08 15:11:35.831 testProj[51550:f803] animation 1st stage
2011-12-08 15:11:37.832 testProj[51550:f803] Animation 2nd stage
2011-12-08 15:11:37.834 testProj[51550:f803] Animation 2nd stage
2011-12-08 15:11:37.848 testProj[51550:f803] All animations complete!
2011-12-08 15:11:39.834 testProj[51550:f803] 2nd stage complete
2011-12-08 15:11:39.851 testProj[51550:f803] 2nd stage complete

Whereas what I need is the "All animations complete!" event to be fired only once all the inner operations have ended.
Perhaps something to do with the fact that I'm only setting completion blocks at the class level and thereby can't tie each block to a specific operation?
In any case I'm still not quite getting it, and using the UIView animateWithDuration overload with it's own completion block argument just throws up the same problem.
Any ideas?

Wrap the whole thing in a CATransaction , and set the transaction's completionBlock . This was my test:

static void setX(UIView *view, CGFloat x)
{
    CGRect frame = view.frame;
    frame.origin.x = x;
    view.frame = frame;
}

- (IBAction)startAnimation:(id)sender {
    label.text = @"Animation starting!";
    setX(redView, 0);
    setX(blueView, 0);
    [CATransaction begin]; {
        [CATransaction setCompletionBlock:^{
            label.text = @"Animation complete!";
        }];
        [UIView animateWithDuration:1 animations:^{
            setX(redView, 300);
        }];
        [UIView animateWithDuration:2 animations:^{
            setX(blueView, 300);
        }];
    } [CATransaction commit];
}

You'll need to add the QuartzCore framework if you haven't already.

Regarding your updated question: since you're creating new animations from completion blocks, those new animations won't be part of the original transaction, so the original transaction won't wait for them to finish.

It would probably be simplest to just start each later animation as part of the original transaction by using +[UIView animateWithDuration:delay:options:animations:completion:] with a delay value that starts the later animation at the right time.

It may help you.

-(void) testAnim2 {
   // NSArray* testArray = [NSArray arrayWithObjects:self.redView, self.blueView, nil];



    [CATransaction begin]; {
        [CATransaction setCompletionBlock:^{

            [CATransaction begin]; {

                [CATransaction setCompletionBlock:^{
                    NSLog(@"All completion block");

                }];


            }[CATransaction commit];

            NSLog(@"animation 2nd stage");
            [UIView animateWithDuration:2 animations:^{
                setX(blueView, 150);
            }];


        }];
        NSLog(@"animation 1st stage");
        [UIView animateWithDuration:2 animations:^{
            setX(redView, 150);


        }];


    } [CATransaction commit];
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM