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

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

Эти анимации заставляют его подниматься или опускаться на определенную величину (скажем, на 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 перед блокировкой.. бесплатно.

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

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

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

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

--

Вывод:

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

touchThreadButton1..

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

tryTheLock afterLock.. ЗАБЛОКИРОВАНО. BEGIN animationBlock - val: 250 touchThreadButton1..

tryTheLock перед блокировкой.. ЗАБЛОКИРОВАНО. * -[Блокировка NSLock]: взаимоблокировка ('(null)') * Прервать _NSLockError() для отладки.

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


person Black Orchid    schedule 24.08.2013    source источник


Ответы (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 анимацию в основной очереди (все обновления пользовательского интерфейса должны выполняться в основной очереди); и

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

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

РазмерОперации.ч:

@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), а не настраивать кадр напрямую; и

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

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

person Rob    schedule 25.08.2013
comment
Спасибо, сэр! Я ценю то, как вы четко демонстрируете NSOperationQueue. Инкапсуляция операции размера в собственном классе довольно элегантна. Это работает отлично. - person Black Orchid; 31.08.2013
comment
@BlackOrchid К вашему сведению, я уверен, что вы уже давно отказались от этого вопроса, но я обновил его, применив значительно более простой подход. - person Rob; 18.09.2014