Создание метода для выполнения анимации и ожидания завершения с использованием семафора в objective c

#ios #objective-c #objective-c-blocks #semaphore #animatewithduration

#iOS #objective-c #objective-c-блоки #семафор #animatewithduration

Вопрос:

Я пытаюсь создать метод, который использует метод UIView » animateWithDuration:animations: completion» для выполнения анимации и ожидания завершения. Я хорошо понимаю, что мог бы просто поместить код, который обычно следует за ним, в блок завершения, но я хотел бы избежать этого, потому что после него есть значительный объем кода, включая дополнительные анимации, что оставило бы меня с вложенными блоками.

Я попытался реализовать этот метод, как показано ниже, используя семафор, но я не думаю, что это лучший способ сделать это, особенно потому, что он на самом деле не работает. Кто-нибудь может сказать мне, что не так с моим кодом и / или каков наилучший способ достижения той же цели?

  (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);
}
  

Я не уверен, что не так с моим кодом, но когда я вызываю метод, как показано ниже, блок завершения никогда не запускается, и я оказываюсь в подвешенном состоянии.

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

——————————————-РЕДАКТИРОВАТЬ————————————————-

Если кто-то сталкивается с подобной проблемой, вам может быть интересно посмотреть код, который я использовал. Он использует рекурсию для использования каждого блока завершения без необходимости вкладывать каждый вызов функции.

  (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] amp;amp; 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];
         }];
    } 
}
  

который можно использовать следующим образом

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

Ответ №1:

В iOS 7 и более поздних версиях для достижения этого эффекта обычно используется анимация ключевых кадров.

Например, двухсекундная анимационная последовательность, состоящая из четырех отдельных анимаций, каждая из которых занимает 25% от всей анимации, будет выглядеть так:

 [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];
  

В более ранних версиях iOS вы могли поставить в очередь серию анимаций несколькими способами, но я бы посоветовал вам избегать использования семафора в основном потоке.

Одним из подходов было бы обернуть анимацию в параллельный NSOperation подкласс, который не завершается, пока не завершится анимация. Затем вы можете добавить свои анимации в свою собственную пользовательскую последовательную очередь:

 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;
}]];
  

AnimationOperation Подкласс может выглядеть так:

 //  AnimationOperation.h

#import <Foundation/Foundation.h>

@interface AnimationOperation : NSOperation

- (instancetype)initWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations;

@end
  

и

 //  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
  

В моей демонстрации выше я использовал последовательную очередь. Но вы также можете использовать параллельную очередь, но использовать NSOperation зависимости для управления взаимосвязью между различными операциями анимации. Здесь много вариантов.


Если вы хотите отменить анимацию, вы можете сделать что-то вроде следующего:

 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
  

Вы также можете включить это в пользовательский cancel метод в операции, если хотите.

Ответ №2:

Похоже, вы используете семафор для блокировки основного потока до тех пор, пока анимации не будут конкурировать. К сожалению, основной поток — это поток, выполняющий эти анимации, поэтому этого никогда не произойдет.

Вы не должны блокировать основной поток вашего приложения. Поступая таким образом, вы предотвращаете рисование любого вида или реагирование на ввод пользователя. Вы также вскоре запустите сторожевой таймер ОС, который обнаружит, что ваше приложение не отвечает, и завершит его.

Анимации и их блоки завершения по уважительной причине выражаются как асинхронные операции. Попробуйте использовать это в своем дизайне.

Ответ №3:

Семафор может решить все проблемы синхронизации блоков.

Два момента

  1. Необходимо создать последовательную очередь.

  2. dispatch_semaphore_wait и dispatch_semaphore_signal не может находиться в одной очереди.


Вот пример

 - (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");
    });
}
  

Ответ №4:

Как сказал Джона, нет способа сделать это в основном потоке. Если вы не хотите иметь вложенные блоки, в этом нет ничего плохого, но я понимаю ваше желание, просто поместите метод внутри блока, а затем внутренний блок в метод. Если вам нужны замыкания во внутреннем блоке, вы можете передать их в качестве аргумента.

Это увеличит вашу любовь к вложенным блокам. 😉

Ответ №5:

Как указывали здесь многие, верно, что анимация выполняется в основном потоке, а использование семафора останавливает основной поток. Но это все еще можно сделать с помощью семафора, используя этот подход:

 // 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

    });

 });