2

我正在尝试创建一个利用 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] && 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]]];
4

5 回答 5

10

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

在早期的 iO​​S 版本中,您可以通过多种方式排列一系列动画,但我建议您避免在主线程上使用信号量。

一种方法是将动画包装在并发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操作中的自定义方法中。

于 2014-06-27T17:40:43.567 回答
4

您似乎正在使用信号量来阻止主线程,直到动画竞争。不幸的是,主线程是执行这些动画的线程,所以这永远不会发生。

您不得阻塞应用程序的主线程。通过这样做,您可以防止任何视图绘制或响应用户输入。您还将很快触发一个操作系统看门狗,它会检测您的应用程序无响应并终止它。

动画及其完成块被表示为异步操作是有充分理由的。尝试在您的设计中接受这一点。

于 2014-06-27T16:24:04.417 回答
1

信号量可以解决所有的块同步问题。

两点

  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");
    });
}
于 2017-04-10T13:59:23.633 回答
0

正如乔纳所说,在主线程上没有办法做到这一点。如果你不想嵌套块,这没什么不好,但我理解你的愿望,只需在块中放置一个方法,然后在方法中放置内部块。如果您需要内部块中的闭包,您可以将它们作为参数传递。

这样做会扩大你对嵌套块的喜爱。;-)

于 2014-06-27T16:47:22.583 回答
0

正如许多人在这里指出的那样,动画在主线程上运行并且信号量使用停止主线程是真的。但这仍然可以通过使用这种方法的信号量来完成:

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

    });

 });
于 2015-05-31T23:50:08.903 回答