Receber notificação quando o NSOperationQueue concluir todas as tarefas

NSOperationQueue tem waitUntilAllOperationsAreFinished , mas não quero esperar em sincronia por isso. Eu só quero esconder o indicador de progresso na interface do usuário quando a fila termina.

Qual é a melhor maneira de conseguir isso?

Não consigo enviar notifications de minhas NSOperation , porque não sei qual delas será a última, e [queue operations] podem não estar vazias ainda (ou pior – repovoadas) quando a notificação é recebida.

Use o KVO para observar a propriedade de operations da sua fila, então você pode dizer se sua fila foi concluída verificando [queue.operations count] == 0 .

Em algum lugar no arquivo que você está fazendo o KVO em, declarar um contexto para KVO como este ( mais informações ):

 static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged"; 

Quando você configurar sua fila, faça o seguinte:

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

Então faça isso em seu 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]; } } 

(Isto está assumindo que o seu NSOperationQueue está em uma propriedade chamada queue )

Em algum momento antes do seu object desalocar totalmente (ou quando ele parar de se preocupar com o estado da fila), você precisará cancelar o registro do KVO desta forma:

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

Adendo: o iOS 4.0 tem uma propriedade NSOperationQueue.operationCount , que, de acordo com os documentos, é compatível com KVO. Essa resposta ainda funcionará no iOS 4.0, por isso ainda é útil para compatibilidade com versões anteriores.

Se você está esperando (ou desejando) algo que corresponda a esse comportamento:

 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  

Você deve estar ciente de que, se várias operações “curtas” estiverem sendo adicionadas a uma fila, você poderá ver esse comportamento (porque as operações são iniciadas como parte da inclusão na fila):

 t=0 add an operation to the queue. queuecount == 1 t=1 operation completes, queuecount decrements to 0  t=2 add an operation to the queue. queuecount == 1 t=3 operation completes, queuecount decrements to 0  t=4 add an operation to the queue. queuecount == 1 t=5 operation completes, queuecount decrements to 0  

No meu projeto, eu precisava saber quando a última operação foi concluída, depois que um grande número de operações foi adicionado a um NSOperationQueue serial (ou seja, maxConcurrentOperationCount = 1) e somente quando todas foram concluídas.

Pesquisando, achei essa declaração de um desenvolvedor da Apple em resposta à pergunta “é uma FIFO serial de NSoperationQueue?” –

Se todas as operações tiverem a mesma prioridade (que não é alterada depois que a operação for incluída em uma fila) e todas as operações forem sempre – isReady == YES no momento em que forem colocadas na fila de operação, uma NSOperationQueue serial será FIFO.

Chris Kane Cocoa Frameworks, Apple

No meu caso, é possível saber quando a última operação foi adicionada à fila. Então, depois que a última operação é adicionada, adiciono outra operação à fila, de menor prioridade, que não faz nada além de enviar a notificação de que a fila foi esvaziada. Dada a declaração da Apple, isso garante que apenas um único aviso seja enviado somente após todas as operações terem sido concluídas.

Se as operações estão sendo adicionadas de uma maneira que não permite a detecção da última (ie, não-determinística) então eu acho que você tem que ir com as abordagens KVO mencionadas acima, com lógica de guarda adicional adicionada para tentar detectar se operações podem ser adicionadas.

🙂

Que tal adicionar uma NSOperation que depende de todas as outras para que seja executada por último?

Uma alternativa é usar o GCD. Consulte isso como referência.

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

É assim que eu faço.

Configure a fila e registre as mudanças na propriedade de operações:

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

… e o observador (nesse 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; } 

Neste exemplo, “spinner” é um UIActivityIndicatorView mostrando que algo está acontecendo. Obviamente você pode mudar para se adequar …

Que tal usar o KVO para observar a propriedade operationCount da fila? Então você ouviria sobre isso quando a fila estivesse vazia, e também quando deixasse de estar vazia. Lidar com o indicador de progresso pode ser tão simples quanto simplesmente fazer algo como:

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

Adicione a última operação como:

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

Assim:

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

Com ReactiveObjC eu acho que isso funciona muito bem:

 // 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!"); } }]; 

FYI, Você pode conseguir isso com o GCD dispatch_group no swift 3 . Você pode ser notificado quando todas as tarefas estiverem concluídas.

 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") } 

Você pode criar um novo NSThread ou executar um seletor em segundo plano e aguardar lá. Quando o NSOperationQueue terminar, você poderá enviar uma notificação por conta própria.

Estou pensando em 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"]; } 

Se você usar essa operação como sua class base, poderá passar o whenEmpty {} para o OperationQueue :

 let queue = OOperationQueue() queue.addOperation(op) queue.addOperation(delayOp) queue.addExecution { finished in delay(0.5) { finished() } } queue.whenEmpty = { print("all operations finished") } 

Eu estou usando uma categoria para fazer isso.

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!) }]; 

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

Sem 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) } }