Как выполнить блокировку, ожидающую завершения анимации?

Я реализую панель состояния, которая анимируется с помощью пользовательского ввода.

Эти анимации заставляют его подниматься или опускаться на определенную величину (скажем, на 50 единиц) и являются результатом нажатия кнопки. Есть две кнопки. Увеличение и уменьшение.

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

Я предполагаю, потому что отдельный поток выполняет блокировку, которая удерживается другим потоком. Но эта блокировка уступит место, когда анимация завершится. Как реализовать блокировку, которая заканчивается, когда завершается [UIView AnimateWithDuration]?

Интересно, если NSConditionLock это путь, но я хочу использовать NSLocks, если это возможно, чтобы избежать ненужной сложности. Что вы порекомендуете?

(В конце концов, я хочу, чтобы анимации "встали в очередь", пока пользовательский ввод продолжается, но сейчас я просто хочу, чтобы блокировка работала, даже если она сначала блокирует ввод.)

(Хм, подумайте об этом, есть только один [UIView AnimateWithDuration], запущенный одновременно для того же самого UIView. Второй вызов прервет первый, в результате чего обработчик завершения будет запущен сразу для первого. Возможно, вторая блокировка запускается до того, как У первого есть шанс разблокировать. Как лучше всего справиться с блокировкой в ​​этом случае? Возможно, мне стоит вернуться к Grand Central Dispatch, но я хотел посмотреть, есть ли более простой способ.)

В ViewController.h я заявляю:

NSLock *_lock;

В ViewController.m у меня есть:

В loadView:

_lock = [[NSLock alloc] init];

Остальная часть ViewController.m (соответствующие части):

-(void)tryTheLockWithStr:(NSString *)str
{
    LLog(@"\n");
    LLog(@" tryTheLock %@..", str);

    if ([_lock tryLock] == NO)
    {
         NSLog(@"LOCKED.");
    }
          else
    {
          NSLog(@"free.");
          [_lock unlock];
    }
}

// TOUCH DECREASE BUTTON
-(void)touchThreadButton1
{
    LLog(@" touchThreadButton1..");

    [self tryTheLockWithStr:@"beforeLock"];
    [_lock lock];
    [self tryTheLockWithStr:@"afterLock"];

    int changeAmtInt = ((-1) * FILLBAR_CHANGE_AMT); 
    [self updateFillBar1Value:changeAmtInt];

    [UIView animateWithDuration:1.0
                      delay:0.0
                    options:(UIViewAnimationOptionTransitionNone|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction)
                 animations:
     ^{

         LLog(@" BEGIN animationBlock - val: %d", self.fillBar1Value)
         self.fillBar1.frame = CGRectMake(FILLBAR_1_X_ORIGIN,FILLBAR_1_Y_ORIGIN, self.fillBar1Value,30);
     }
     completion:^(BOOL finished)
     {
         LLog(@" END animationBlock - val: %d - finished: %@", self.fillBar1Value, (finished ? @"YES" : @"NO"));

         [self tryTheLockWithStr:@"beforeUnlock"];
         [_lock unlock];
         [self tryTheLockWithStr:@"afterUnlock"];         
     }
     ];
}

-(void)updateFillBar1Value:(int)changeAmt
{
    self.prevFillBar1Value = self.fillBar1Value;

    self.fillBar1Value += changeAmt;

    if (self.fillBar1Value < FILLBAR_MIN_VALUE)
    {
        self.fillBar1Value = FILLBAR_MIN_VALUE;
    }
    else if (self.fillBar1Value > FILLBAR_MAX_VALUE)
    {
        self.fillBar1Value = FILLBAR_MAX_VALUE;
    }
}

Выход:

Чтобы воспроизвести инструкции: нажмите "Уменьшить" один раз

touchThreadButton1..

попробуйте TheLock beforeLock.. бесплатно.

tryTheLock afterLock.. ЗАБЛОКИРОВАНО. BEGIN animationBlock - значение: 250 END animationBlock - значение: 250 - завершено: ДА

попробуй заблокировать до разблокировки.. ЗАБЛОКИРОВАН.

попробуйте theLock после разблокировки.. бесплатно.

Вывод: это работает как ожидалось.

-

Выход:

Чтобы воспроизвести инструкции: быстро нажмите "Уменьшить" дважды (прерывая первоначальную анимацию).

touchThreadButton1..

попробуйте TheLock beforeLock.. бесплатно.

tryTheLock afterLock.. ЗАБЛОКИРОВАНО. НАЧАТЬ анимациюБлок - значение: 250 TouchThreadButton1..

попробуйте TheLock beforeLock.. ЗАБЛОКИРОВАНО. * - [NSLock lock]: deadlock ('(null)') * Сбой _NSLockError() для отладки.

Заключение. Ошибка тупика. Пользовательский ввод заморожен.

1 ответ

Решение

Внизу, в моем исходном ответе, я описываю способ достижения запрошенной функциональности (если вы запускаете анимацию, пока предыдущая анимация еще выполняется, поставьте в очередь эту последующую анимацию, чтобы запускать ее только после завершения текущих).

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

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

    Типичное решение в версиях iOS до 8 будет:

    • захватить presentationLayer анимированного представления (это текущее состояние CALayer из UIView... если вы посмотрите на UIView пока идет анимация, вы увидите окончательное значение, и нам нужно получить текущее состояние);

    • захватить текущее значение анимированного значения свойства из этого presentationLayer;

    • удалить анимацию;

    • сбросить анимированное свойство к "текущему" значению (чтобы оно не переходило в конец предыдущей анимации перед началом следующей анимации);

    • инициировать анимацию до "нового" значения;

    Так, например, если вы анимируете изменение frame который может быть в процессе анимации, вы можете сделать что-то вроде:

    CALayer *presentationLayer = animatedView.layer.presentationLayer;
    CGRect currentFrame = presentationLayer.frame;
    [animatedView.layer removeAllAnimations];
    animatedView.frame = currentFrame;
    [UIView animateWithDuration:1.0 animations:^{
        animatedView.frame = newFrame;
    }];
    

    Это полностью устраняет всю неловкость, связанную с постановкой в ​​очередь "следующей" анимации, запускаемой после завершения "текущей" анимации (и других анимаций в очереди). Вы также получите гораздо более отзывчивый пользовательский интерфейс (например, вам не нужно ждать завершения предыдущих анимаций, прежде чем начнется желаемая анимация пользователя).

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

    Для получения дополнительной информации об этой новой функции iOS 8, я бы посоветовал вам обратиться к видео WWDC 2014 Построение прерывистых и гибких взаимодействий.

Для полноты изложения я оставлю свой первоначальный ответ ниже, так как он пытается точно использовать функциональность, изложенную в вопросе (просто используется другой механизм, чтобы гарантировать, что основная очередь не заблокирована). Но я бы действительно посоветовал рассмотреть возможность остановки текущей анимации и запуска новой таким образом, чтобы она начиналась с того места, где могла закончиться какая-либо анимация в процессе.


Оригинальный ответ:

Я бы не рекомендовал оборачивать анимацию в NSLock (или семафор, или любой другой подобный механизм), потому что это может привести к блокировке основного потока. Вы никогда не хотите блокировать основной поток. Я думаю, что ваша интуиция об использовании последовательной очереди для изменения размера операций является многообещающей. И вы, вероятно, хотите операцию "изменения размера", которая:

  • инициирует UIView анимация в главной очереди (все обновления пользовательского интерфейса должны происходить в главной очереди); а также

  • в блоке завершения анимации завершите операцию (мы не завершаем операцию до тех пор, пока другие операции из очереди не начнутся до тех пор, пока эта операция не будет завершена).

Я мог бы предложить операцию изменения размера:

SizeOperation.h:

@interface SizeOperation : NSOperation

@property (nonatomic) CGFloat sizeChange;
@property (nonatomic, weak) UIView *view;

- (id)initWithSizeChange:(NSInteger)change view:(UIView *)view;

@end

SizingOperation.m:

#import "SizeOperation.h"

@interface SizeOperation ()

@property (nonatomic, readwrite, getter = isFinished)  BOOL finished;
@property (nonatomic, readwrite, getter = isExecuting) BOOL executing;

@end

@implementation SizeOperation

@synthesize finished = _finished;
@synthesize executing = _executing;

- (id)initWithSizeChange:(NSInteger)change view:(UIView *)view
{
    self = [super init];
    if (self) {
        _sizeChange = change;
        _view = view;
    }
    return self;
}

- (void)start
{
    if ([self isCancelled] || self.view == nil) {
        self.finished = YES;
        return;
    }

    self.executing = YES;

    // note, UI updates *must* take place on the main queue, but in the completion
    // block, we'll terminate this particular operation

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [UIView animateWithDuration:2.0 delay:0.0 options:kNilOptions animations:^{
            CGRect frame = self.view.frame;
            frame.size.width += self.sizeChange;
            self.view.frame = frame;
        } completion:^(BOOL finished) {
            self.finished = YES;
            self.executing = NO;
        }];
    }];
}

#pragma mark - NSOperation methods

- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

@end

Затем определите очередь для этих операций:

@property (nonatomic, strong) NSOperationQueue *sizeQueue;

Убедитесь, что создали экземпляр этой очереди (как последовательная очередь):

self.sizeQueue = [[NSOperationQueue alloc] init];
self.sizeQueue.maxConcurrentOperationCount = 1;

И тогда, все, что делает рассматриваемый вид, будет расти:

[self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:+50.0 view:self.barView]];

И все, что сокращает видимость, сделало бы:

[self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:-50.0 view:self.barView]];

Надеюсь, это иллюстрирует идею. Есть всевозможные уточнения:

  • Я сделал анимацию очень медленной, поэтому я мог легко поставить в очередь целую кучу, но вы, вероятно, использовали бы гораздо более короткое значение;

  • Если вы используете автоматическое расположение, вы бы отрегулировали ограничение ширины constant и в блоке анимации вы бы выполнить layoutIfNeeded) вместо того, чтобы регулировать рамку напрямую; а также

  • Возможно, вы захотите добавить проверки, чтобы не выполнять смену кадра, если ширина достигла некоторых максимальных / минимальных значений.

Но ключ в том, что использование блокировок для управления анимацией изменений пользовательского интерфейса нецелесообразно. Вам не нужно ничего, что могло бы блокировать основную очередь на что-либо, кроме нескольких миллисекунд. Анимационные блоки слишком длинные, чтобы предусмотреть блокировку основной очереди. Поэтому используйте очередь последовательных операций (и если у вас есть несколько потоков, которые должны инициировать изменения, они просто добавят операцию в одну и ту же очередь общих операций, тем самым автоматически координируя изменения, инициированные из всех видов различных потоков).

Другие вопросы по тегам