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

后端 未结 5 1601
没有蜡笔的小新
没有蜡笔的小新 2020-12-12 01:02

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 a

相关标签:
5条回答
  • 2020-12-12 01:10

    You appear to be using a semaphore to block the main thread until animations compete. Unfortunately the main thread is the thread performing those animations so this will never occur.

    You must not block the main thread of your application. By doing so you prevent any view drawing or responding to user input. You will also soon trigger an OS watchdog which detects your app as unresponsive and terminates it.

    Animations an their completion blocks are expressed as asynchronous operations for good reason. Try to embrace that in your design.

    0 讨论(0)
  • 2020-12-12 01:15

    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.

    0 讨论(0)
  • 2020-12-12 01:23

    As many people pointed out here, is true that the animation runs on the main thread and the semaphore usage stops the main thread. But this still can be done with a semaphore using this approach:

    // create semaphore
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
    
        // do animations
    
    } completion:^(BOOL finished) {
    
         // send signal
        dispatch_semaphore_signal(semaphore);
    
    }];
    
    // create background execution to avoid blocking the main thread
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
        // wait for the semaphore    
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
        // now create a main thread execution
        dispatch_async(dispatch_get_main_queue(), ^{
    
            // continue main thread logic
    
        });
    
     });
    
    0 讨论(0)
  • 2020-12-12 01:24

    Semaphore can solve all the block synchronization problems.

    Two points

    1. Need to create a serial queue.

    2. dispatch_semaphore_wait and dispatch_semaphore_signal can not be in the same queue.


    Here is a example

    - (dispatch_queue_t)queue {
        if (!_queue) {
            _queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
        }
        return _queue;
    }
    
    - (dispatch_semaphore_t)semaphore {
        if (!_semaphore) {
            _semaphore = dispatch_semaphore_create(0);
        }
        return _semaphore;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        [self funcA];
        [self funcA];
        [self funcA];
        [self funcA];
        [self funcA];
        [self funcB];    
    }
    
    - (void)funcA {
        dispatch_async(self.queue, ^{
    
            //Switch to main queue to perform animation
            dispatch_async(dispatch_get_main_queue(), ^{
                self.view.alpha = 1;
                [UIView animateWithDuration:2 animations:^{
                    self.view.alpha = 0;
                } completion:^(BOOL finished) {
                    NSLog(@"funcA completed");
                    //Unblock custom queue
                    dispatch_semaphore_signal(self.semaphore);
                }];
            });
    
            //Block custom queue
            dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        });
    }
    
    - (void)funcB {
        dispatch_async(self.queue, ^{
            NSLog(@"funcB completed");
        });
    }
    
    0 讨论(0)
  • 2020-12-12 01:29

    As Jonah said, there is no way to do that on the main thread. If you do not want to have nested blocks, there is nothing bad with it, but I understand your wish, simply put a method inside the block and the inner block then in the method. If you need closures in the inner block, you can pass them as an argument.

    Doing so will enlarge your love to nested blocks. ;-)

    0 讨论(0)
提交回复
热议问题