¿Cómo ejecutar un locking que espera hasta que se complete una animation?

Estoy implementando una barra de salud que se anima a través de la input del usuario.

Estas animaciones lo hacen upload o bajar una cierta cantidad (digamos 50 unidades) y son el resultado de un button presionar. Hay dos botones Aumentar y disminuir.

Quiero ejecutar un locking en la barra de estado para que solo un hilo pueda cambiarlo a la vez. El problema es que estoy teniendo un punto muerto.

Estoy adivinando porque un subprocess independiente ejecuta un locking que es retenido por otro subprocess. Pero ese locking cederá cuando se complete la animation. ¿Cómo implementa un locking que finaliza cuando [UIView AnimateWithDuration] se completa?

Me pregunto si NSConditionLock es el path a seguir, pero quiero usar NSLocks si es posible para evitar la complejidad innecesaria. ¿Que recomiendas?

(Eventualmente quiero tener las animaciones "poner en queue" mientras se deja que la input del usuario continúe, pero por el momento solo quiero que funcione el locking, incluso si primero bloquea la input).

(Hmm, pienso que solo hay una [UIView AnimateWithDuration] ejecutándose a la vez para la misma UIView. Una segunda llamada interrumpirá la primera, lo que hará que el manejador de finalización se ejecute inmediatamente por la primera. Tal vez el segundo locking se ejecute antes de la primero tiene la posibilidad de desbloquear. ¿Cuál es la mejor manera de manejar el locking en este caso? Tal vez debería volver a visitar Grand Central Dispatch pero quería ver si había una manera más simple).

En ViewController.h declaro:

 NSLock *_lock; 

En ViewController.m tengo:

En loadView:

 _lock = [[NSLock alloc] init]; 

El rest de ViewController.m (partes relevantes):

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

Salida:

Instrucciones para reproducir: toque "Disminuir" una vez

touchThreadButton1 ..

tryTheLock beforeLock .. gratis

tryTheLock afterLock .. BLOQUEADO. BEGIN animationBlock – val: 250 END animationBlock – val: 250 – terminado: YES

tryTheLock beforeUnlock .. BLOQUEADO.

tryTheLock afterUnlock .. gratis.

Conclusión: Esto funciona como se esperaba.

Salida:

Instrucciones para reproducir: toque "Disminuir" dos veces rápidamente (interrumpiendo la animation inicial) ..

touchThreadButton1 ..

tryTheLock beforeLock .. gratis

tryTheLock afterLock .. BLOQUEADO. BEGIN animationBlock – val: 250 touchThreadButton1 ..

tryTheLock beforeLock .. BLOQUEADO. * – [NSLock lock]: deadlock ('(null)') * Rompe en _NSLockError () para depurar.

Conclusión. Error de interlocking La input del usuario está congelada.

En la parte inferior, en mi respuesta original, describo una forma de lograr la funcionalidad solicitada (si inicia una animation mientras la animation anterior aún está en progreso, ponga en queue esta animation subsiguiente para comenzar solo después de que se hayan realizado las actuales).

Si bien lo mantendré para fines históricos, podría sugerir un enfoque completamente diferente. Específicamente, si tocas un button que debería dar como resultado una animation, pero una animation previa aún está en progreso, sugeriría que elimines la animation anterior e inicie inmediatamente la nueva animation, pero hazlo de manera tal que el nuevo La animation se retoma desde donde se quedó la actual.

  1. En las versiones de iOS anteriores al iOS 8, el desafío es que si comienzas una nueva animation mientras otra está en progreso, el sistema operativo saltará de manera incómoda de inmediato a donde la animation actual habría terminado e iniciará la nueva animation desde allí.

    La solución típica en las versiones de iOS anteriores al 8 sería:

    • toma la presentationLayer de la presentationLayer de la vista animada (este es el estado actual de CALayer de la vista UIView … si miras la vista UIView mientras la animation está en progreso, verás el valor final y tenemos que capturar la actual estado);

    • toma el valor actual del valor de la propiedad animada de esa presentationLayer .

    • eliminar las animaciones;

    • restablecer la propiedad animada al valor "actual" (para que no parezca saltar al final de la animation anterior antes de comenzar la siguiente animation);

    • inicie la animation al valor "nuevo";

    Por ejemplo, si está animando el cambio de un frame que podría estar en progreso de una animation, puede hacer algo como:

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

    Esto elimina por completo toda la incomodidad asociada con poner en queue la animation "siguiente" para ejecutar después de que se complete la animation "actual" (y otras animaciones en queue). También obtienes una interfaz de usuario mucho más receptiva (por ejemplo, no tienes que esperar a que las animaciones anteriores terminen antes de que comience la animation deseada del usuario).

  2. En iOS 8, este process es infinitamente más fácil, donde, si inicia una nueva animation, a menudo comenzará la animation no solo del valor actual de la propiedad animada, sino que también identificará la velocidad a la que está animada actualmente la propiedad está cambiando, lo que resulta en una transición perfecta entre la animation antigua y la nueva animation.

    Para get más información acerca de esta nueva function de iOS 8, sugiero que se refiera al video WWDC 2014 Building Interrupt Interrupt Responsive .

En aras de la exhaustividad, mantendré mi respuesta original a continuación, ya que trata de abordar con precisión la funcionalidad descrita en la pregunta (solo usa un mecanismo diferente para garantizar que la queue principal no esté bloqueada). Pero realmente sugiero considerar detener la animation actual e iniciar la nueva de tal manera que comience desde donde quede cualquier animation en curso.


Respuesta original:

No recomendaría envolver una animation en un NSLock (o semáforo, u otro mecanismo similar) porque eso puede resultar en bloquear el hilo principal. Nunca quieres bloquear el hilo principal. Creo que su intuición sobre el uso de una queue de serie para cambiar el tamaño de las operaciones es prometedora. Y es probable que desee una operación de "cambio de tamaño" que:

  • inicia la animation UIView en la queue principal (todas las actualizaciones de la interfaz de usuario deben llevarse a cabo en la queue principal); y

  • en el bloque de finalización de la animation, termine la operación (no terminamos la operación hasta entonces, para garantizar que otras operaciones en queue no se inicien hasta que ésta finalice).

Yo podría sugerir una operación de cambio de tamaño:

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 

Luego defina una queue para estas operaciones:

 @property (nonatomic, strong) NSOperationQueue *sizeQueue; 

Asegúrese de crear una instancia de esta queue (como una queue de serie):

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

Y luego, cualquier cosa que haga crecer la vista en cuestión, haría:

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

Y cualquier cosa que haga que la vista en cuestión se networkinguzca, haría:

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

Con suerte, esto ilustra la idea. Hay todo tipo de mejoras posibles:

  • Hice la animation muy lenta, así que pude poner en queue un montón, pero probablemente usarías un valor mucho más corto;

  • Si usa el layout automático, estaría ajustando la constant la restricción de ancho y en el bloque de animation se realizaría un layoutIfNeeded ) en lugar de ajustar el marco directamente; y

  • Probablemente quiera agregar cheques para no realizar el cambio de marco si el ancho ha alcanzado algunos valores máximos / mínimos.

Pero la key es que el uso de lockings para controlar la animation de los cambios de interfaz de usuario no es aconsejable. No quieres nada que pueda bloquear la queue principal por nada más que unos pocos milisegundos. Los bloques de animation son demasiado largos para contemplar el locking de la queue principal. Utilice una queue de operaciones en serie (y si tiene varios subprocesss que necesitan iniciar cambios, todos agregarán una operación a la misma queue de operaciones compartidas, coordinando automáticamente los cambios iniciados de todo tipo de subprocesss diferentes).