iosobjective-cobjective-c-blockssemaphoreanimatewithduration

Creating a method to perform animations and wait for completion using a semaphore in objective c


I am trying to create a method which makes use of UIView's "+animateWithDuration:animations:completion" method to perform animations, and wait for completion. I am well aware that I could just place the code that would normally come after it in a completion block, but I would like to avoid this because there is a substantial amount of code after it including more animations, which would leave me with nested blocks.

I tried to implement this method as below using a semaphore, but I don't think this is the best way to do it, especially because it doesn't actually work. Can anyone tell me what is wrong with my code, and/or what the best way of achieving the same goal is?

+(void)animateAndWaitForCompletionWithDuration:(NSTimeInterval)duration animations:(void  (^)(void))animations
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [UIView animateWithDuration:duration
                 animations:animations
                 completion:^(BOOL finished) {
                     dispatch_semaphore_signal(semaphore);
                 }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

I'm not sure what's wrong with my code but when I call the method as shown below, the completion block never runs and I end up in limbo.

[Foo animateAndWaitForCompletionWithDuration:0.5 animations:^{
    //do stuff
}];

-------------------------------------------EDIT-------------------------------------------------

If anyone is facing a similar issue, you might be interested to see the code I used. It uses recursion to make use of each completion block, without having to nest each function call.

+(void)animateBlocks:(NSArray*)animations withDurations:(NSArray*)durations {
    [Foo animateBlocks:animations withDurations:durations atIndex:0];
}

+(void)animateBlocks:(NSArray*)animations withDurations:(NSArray*)durations atIndex:(NSUInteger)i {
    if (i < [animations count] && i < [durations count]) {
         [UIView animateWithDuration:[(NSNumber*)durations[i] floatValue]
                          animations:(void(^)(void))animations[i]
                          completion:^(BOOL finished){
             [Foo animateBlocks:animations withDurations:durations atIndex:i+1];
         }];
    } 
}

which can be used like so

[Foo animateBlocks:@[^{/*first animation*/},
                     ^{/*second animation*/}]
     withDurations:@[[NSNumber numberWithFloat:2.0],
                     [NSNumber numberWithFloat:2.0]]];

Solution

  • In iOS 7 and later one would generally employ keyframe animation to achieve this effect.

    For example, a two second animation sequence that is composed of four separate animations that take up 25% of the entire animation each would look like:

    [UIView animateKeyframesWithDuration:2.0 delay:0.0 options:UIViewKeyframeAnimationOptionRepeat animations:^{
        [UIView addKeyframeWithRelativeStartTime:0.00 relativeDuration:0.25 animations:^{
            viewToAnimate.frame = ...;
        }];
        [UIView addKeyframeWithRelativeStartTime:0.25 relativeDuration:0.25 animations:^{
            viewToAnimate.frame = ...;
        }];
        [UIView addKeyframeWithRelativeStartTime:0.50 relativeDuration:0.25 animations:^{
            viewToAnimate.frame = ...;
        }];
        [UIView addKeyframeWithRelativeStartTime:0.75 relativeDuration:0.25 animations:^{
            viewToAnimate.frame = ...;
        }];
    } completion:nil];
    

    In earlier iOS versions, you could queue up a series of animations a couple of ways, but I'd encourage you to avoid using a semaphore on the main thread.

    One approach would be to wrap the animation in a concurrent NSOperation subclass, which doesn't complete until the animation does. You can then add your animations to your own custom serial queue:

    NSOperationQueue *animationQueue = [[NSOperationQueue alloc] init];
    animationQueue.maxConcurrentOperationCount = 1;
    
    [animationQueue addOperation:[[AnimationOperation alloc] initWithDuration:1.0 delay:0.0 options:0 animations:^{
        viewToAnimate.center = point1;
    }]];
    
    [animationQueue addOperation:[[AnimationOperation alloc] initWithDuration:1.0 delay:0.0 options:0 animations:^{
        viewToAnimate.center = point2;
    }]];
    
    [animationQueue addOperation:[[AnimationOperation alloc] initWithDuration:1.0 delay:0.0 options:0 animations:^{
        viewToAnimate.center = point3;
    }]];
    
    [animationQueue addOperation:[[AnimationOperation alloc] initWithDuration:1.0 delay:0.0 options:0 animations:^{
        viewToAnimate.center = point4;
    }]];
    

    The AnimationOperation subclass might look like:

    //  AnimationOperation.h
    
    #import <Foundation/Foundation.h>
    
    @interface AnimationOperation : NSOperation
    
    - (instancetype)initWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations;
    
    @end
    

    and

    //  AnimationOperation.m
    
    #import "AnimationOperation.h"
    
    @interface AnimationOperation ()
    
    @property (nonatomic, readwrite, getter = isFinished)  BOOL finished;
    @property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
    
    @property (nonatomic, copy) void (^animations)(void);
    @property (nonatomic) UIViewAnimationOptions options;
    @property (nonatomic) NSTimeInterval duration;
    @property (nonatomic) NSTimeInterval delay;
    
    @end
    
    @implementation AnimationOperation
    
    @synthesize finished  = _finished;
    @synthesize executing = _executing;
    
    - (instancetype)initWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations {
        self = [super init];
        if (self) {
            _animations = animations;
            _options    = options;
            _delay      = delay;
            _duration   = duration;
        }
        return self;
    }
    
    - (void)start {
        if ([self isCancelled]) {
            self.finished = YES;
            return;
        }
    
        self.executing = YES;
    
        [self main];
    }
    
    - (void)main {
        dispatch_async(dispatch_get_main_queue(), ^{
            [UIView animateWithDuration:self.duration delay:self.delay options:self.options animations:self.animations completion:^(BOOL finished) {
                [self completeOperation];
            }];
        });
    }
    
    #pragma mark - NSOperation methods
    
    - (void)completeOperation {
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }
    
    - (BOOL)isAsynchronous {
        return YES;
    }
    
    - (BOOL)isExecuting {
        @synchronized(self) { return _executing; }
    }
    
    - (BOOL)isFinished {
        @synchronized(self) { return _finished; }
    }
    
    - (void)setExecuting:(BOOL)executing {
        if (_executing != executing) {
            [self willChangeValueForKey:@"isExecuting"];
            @synchronized(self) { _executing = executing; }
            [self didChangeValueForKey:@"isExecuting"];
        }
    }
    
    - (void)setFinished:(BOOL)finished {
        if (_finished != finished) {
            [self willChangeValueForKey:@"isFinished"];
            @synchronized(self) { _finished = finished; }
            [self didChangeValueForKey:@"isFinished"];
        }
    }
    
    @end
    

    In my demonstration, above, I used a serial queue. But you could also use a concurrent queue, but use NSOperation dependencies to manage the relationship between the various animation operations. Lots of options here.


    If you want to cancel the animations, you could then do something like the following:

    CALayer *layer = [viewToAnimate.layer presentationLayer];
    CGRect frame = layer.frame;                // identify where it is now
    [animationQueue cancelAllOperations];      // cancel the operations
    [viewToAnimate.layer removeAllAnimations]; // cancel the animations
    viewToAnimate.frame = frame;               // set the final position to where it currently is
    

    You could also incorporate this into a custom cancel method in the operation, too, if you want.