Pregunta

NSOperationQueue tiene waitUntilAllOperationsAreFinished, pero no quiero esperar sincrónicamente. Solo quiero ocultar el indicador de progreso en la interfaz de usuario cuando finalice la cola.

¿Cuál es la mejor manera de lograr esto?

No puedo enviar notificaciones desde mis NSOperation s, porque no sé cuál va a ser el último, y es posible que [queue operations] todavía no esté vacío (o peor aún, repoblado) cuando se recibe la notificación.

¿Fue útil?

Solución

Use KVO para observar la propiedad operations de su cola, luego puede saber si su cola se ha completado marcando [queue.operations count] == 0.

En algún lugar del archivo en el que está haciendo el KVO, declare un contexto para KVO como este ( más información ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Cuando configure su cola, haga esto:

[self.queue addObserver:self forKeyPath:@"operations" options:0 context:&kQueueOperationsChanged];

Luego haz esto en tu observeValueForKeyPath:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
                         change:(NSDictionary *)change context:(void *)context
{
    if (object == self.queue && [keyPath isEqualToString:@"operations"] && context == &kQueueOperationsChanged) {
        if ([self.queue.operations count] == 0) {
            // Do something here when your queue has completed
            NSLog(@"queue has completed");
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object 
                               change:change context:context];
    }
}

(Esto supone que su NSOperationQueue está en una propiedad llamada queue)

En algún momento antes de que su objeto se desplace por completo (o cuando deje de preocuparse por el estado de la cola), deberá cancelar el registro de KVO de esta manera:

[self.queue removeObserver:self forKeyPath:@"operations" context:&kQueueOperationsChanged];


Anexo: iOS 4.0 tiene una propiedad NSOperationQueue.operationCount, que según los documentos es compatible con KVO. Sin embargo, esta respuesta seguirá funcionando en iOS 4.0, por lo que sigue siendo útil para la compatibilidad con versiones anteriores.

Otros consejos

Si espera (o desea) algo que coincida con este comportamiento:

t=0 add an operation to the queue.  queueucount increments to 1
t=1 add an operation to the queue.  queueucount increments to 2
t=2 add an operation to the queue.  queueucount increments to 3
t=3 operation completes, queuecount decrements to 2
t=4 operation completes, queuecount decrements to 1
t=5 operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>

Debe tener en cuenta que si un número de " short " las operaciones se están agregando a una cola, puede ver este comportamiento en su lugar (porque las operaciones se inician como parte de la adición a la cola):

t=0  add an operation to the queue.  queuecount == 1
t=1  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>
t=2  add an operation to the queue.  queuecount == 1
t=3  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>
t=4  add an operation to the queue.  queuecount == 1
t=5  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>

En mi proyecto, necesitaba saber cuándo se completó la última operación, después de que se había agregado una gran cantidad de operaciones a un NSOperationQueue en serie (es decir, maxConcurrentOperationCount = 1) y solo cuando se habían completado todas.

Buscar en Google Encontré esta afirmación de un desarrollador de Apple en respuesta a la pregunta " es un NSoperationQueue FIFO serial? " -

  

Si todas las operaciones tienen la misma prioridad (que no cambia después de   la operación se agrega a una cola) y todas las operaciones son siempre:   isReady == YES en el momento en que se ponen en la cola de operaciones, luego un serial   NSOperationQueue es FIFO.

     

Chris Kane   Marcos de cacao, manzana

En mi caso, es posible saber cuándo se agregó la última operación a la cola. Entonces, después de agregar la última operación, agrego otra operación a la cola, de menor prioridad, que no hace más que enviar la notificación de que la cola se ha vaciado. Dada la declaración de Apple, esto garantiza que solo se envíe un solo aviso después de que se hayan completado todas las operaciones.

Si las operaciones se agregan de una manera que no permite detectar la última, (es decir, no determinista), entonces creo que debe seguir los enfoques de KVO mencionados anteriormente, con lógica de protección adicional agregada para intentar detectar si se pueden agregar más operaciones.

:)

¿Qué tal si agrega una operación NSO que depende de todas las demás para que se ejecute en último lugar?

Una alternativa es usar GCD. Consulte esto como referencia.

dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,queue,^{
 NSLog(@"Block 1");
 //run first NSOperation here
});

dispatch_group_async(group,queue,^{
 NSLog(@"Block 2");
 //run second NSOperation here
});

//or from for loop
for (NSOperation *operation in operations)
{
   dispatch_group_async(group,queue,^{
      [operation start];
   });
}

dispatch_group_notify(group,queue,^{
 NSLog(@"Final block");
 //hide progress indicator here
});

Así es como lo hago.

Configure la cola y regístrese para los cambios en la propiedad de operaciones:

myQueue = [[NSOperationQueue alloc] init];
[myQueue addObserver: self forKeyPath: @"operations" options: NSKeyValueObservingOptionNew context: NULL];

... y el observador (en este caso self) implementa:

- (void) observeValueForKeyPath:(NSString *) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void *) context {

    if (
        object == myQueue
        &&
        [@"operations" isEqual: keyPath]
    ) {

        NSArray *operations = [change objectForKey:NSKeyValueChangeNewKey];

        if ( [self hasActiveOperations: operations] ) {
            [spinner startAnimating];
        } else {
            [spinner stopAnimating];
        }
    }
}

- (BOOL) hasActiveOperations:(NSArray *) operations {
    for ( id operation in operations ) {
        if ( [operation isExecuting] && ! [operation isCancelled] ) {
            return YES;
        }
    }

    return NO;
}

En este ejemplo " spinner " es un UIActivityIndicatorView que muestra que algo está sucediendo. Obviamente puede cambiar para adaptarse ...

¿Qué pasa con el uso de KVO para observar la propiedad operationCount de la cola? Entonces se enteraría de ello cuando la cola se quedara vacía y también cuando dejara de estar vacía. Tratar con el indicador de progreso puede ser tan simple como hacer algo como:

[indicator setHidden:([queue operationCount]==0)]

Agregue la última operación como:

NSInvocationOperation *callbackOperation = [[NSInvocationOperation alloc] initWithTarget:object selector:selector object:nil];

Entonces:

- (void)method:(id)object withSelector:(SEL)selector{
     NSInvocationOperation *callbackOperation = [[NSInvocationOperation alloc] initWithTarget:object selector:selector object:nil];
     [callbackOperation addDependency: ...];
     [operationQueue addOperation:callbackOperation]; 

}

Con ReactiveObjC creo que esto funciona muy bien:

// skip 1 time here to ignore the very first call which occurs upon initialization of the RAC block
[[RACObserve(self.operationQueue, operationCount) skip:1] subscribeNext:^(NSNumber *operationCount) {
    if ([operationCount integerValue] == 0) {
         // operations are done processing
         NSLog(@"Finished!");
    }
}];

Para su información, puede lograr esto con GCD dispatch_group en swift 3 . Puede recibir una notificación cuando finalicen todas las tareas.

let group = DispatchGroup()

    group.enter()
    run(after: 6) {
      print(" 6 seconds")
      group.leave()
    }

    group.enter()
    run(after: 4) {
      print(" 4 seconds")
      group.leave()
    }

    group.enter()
    run(after: 2) {
      print(" 2 seconds")
      group.leave()
    }

    group.enter()
    run(after: 1) {
      print(" 1 second")
      group.leave()
    }


    group.notify(queue: DispatchQueue.global(qos: .background)) {
      print("All async calls completed")
}

Estoy usando una categoría para hacer esto.

NSOperationQueue+Completion.h

//
//  NSOperationQueue+Completion.h
//  QueueTest
//
//  Created by Artem Stepanenko on 23.11.13.
//  Copyright (c) 2013 Artem Stepanenko. All rights reserved.
//

typedef void (^NSOperationQueueCompletion) (void);

@interface NSOperationQueue (Completion)

/**
 * Remarks:
 *
 * 1. Invokes completion handler just a single time when previously added operations are finished.
 * 2. Completion handler is called in a main thread.
 */

- (void)setCompletion:(NSOperationQueueCompletion)completion;

@end

NSOperationQueue+Completion.m

//
//  NSOperationQueue+Completion.m
//  QueueTest
//
//  Created by Artem Stepanenko on 23.11.13.
//  Copyright (c) 2013 Artem Stepanenko. All rights reserved.
//

#import "NSOperationQueue+Completion.h"

@implementation NSOperationQueue (Completion)

- (void)setCompletion:(NSOperationQueueCompletion)completion
{
    NSOperationQueueCompletion copiedCompletion = [completion copy];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self waitUntilAllOperationsAreFinished];

        dispatch_async(dispatch_get_main_queue(), ^{
            copiedCompletion();
        });
    });
}

@end

Uso :

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    // ...
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    // ...
}];

[operation2 addDependency:operation1];

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation1, operation2] waitUntilFinished:YES];

[queue setCompletion:^{
    // handle operation queue's completion here (launched in main thread!)
}];

Fuente: https://gist.github.com/artemstepanenko/7620471

Puede crear un nuevo NSThread o ejecutar un selector en segundo plano y esperar allí. Cuando finalice el NSOperationQueue, puede enviar una notificación propia.

Estoy pensando en algo como:

- (void)someMethod {
    // Queue everything in your operationQueue (instance variable)
    [self performSelectorInBackground:@selector(waitForQueue)];
    // Continue as usual
}

...

- (void)waitForQueue {
    [operationQueue waitUntilAllOperationsAreFinished];
    [[NSNotificationCenter defaultCenter] postNotification:@"queueFinished"];
}

Si usa esta Operación como su clase base, podría pasar el bloque whenEmpty {} al OperationQueue :

let queue = OOperationQueue()
queue.addOperation(op)
queue.addOperation(delayOp)

queue.addExecution { finished in
    delay(0.5) { finished() }
}

queue.whenEmpty = {
    print("all operations finished")
}

Sin KVO

private let queue = OperationQueue()

private func addOperations(_ operations: [Operation], completionHandler: @escaping () -> ()) {
    DispatchQueue.global().async { [unowned self] in
        self.queue.addOperations(operations, waitUntilFinished: true)
        DispatchQueue.main.async(execute: completionHandler)
    }
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top