Obtenir une notification lorsque NSOperationQueue termine toutes les tâches

NSOperationQueue a waitUntilAllOperationsAreFinished , mais je ne veux pas l’attendre de manière synchrone. Je veux juste masquer l’indicateur de progression dans l’interface utilisateur lorsque la queue se termine.

Quelle est la meilleure façon d’y parvenir?

Je ne peux pas envoyer de notifications à partir de NSOperation , car je ne sais pas laquelle sera la dernière, et il se peut que [queue operations] ne soient pas encore vides (ou pire encore) lorsque la notification est reçue.

Utilisez KVO pour observer la propriété operations de votre queue, puis vous pourrez savoir si votre queue est terminée en recherchant [queue.operations count] == 0 .

Quelque part dans le fichier dans lequel vous faites le KVO, déclarez un contexte pour KVO comme ceci ( plus d’infos ):

 static NSSsortingng *kQueueOperationsChanged = @"kQueueOperationsChanged"; 

Lorsque vous configurez votre queue, procédez comme suit:

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

Ensuite, faites ceci dans votre observeValueForKeyPath :

 - (void) observeValueForKeyPath:(NSSsortingng *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == self.queue && [keyPath isEqualToSsortingng:@"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]; } } 

(Cela suppose que votre NSOperationQueue trouve dans une propriété nommée queue )

À un moment donné, avant que votre object ne soit complètement libéré (ou lorsqu’il ne se préoccupe plus de l’état de la queue), vous devrez vous désinscrire de KVO comme ceci:

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

Addendum: iOS 4.0 a une propriété NSOperationQueue.operationCount , qui, selon les documents, est conforme à KVO. Cette réponse fonctionnera toujours sous iOS 4.0, elle est donc toujours utile pour la compatibilité ascendante.

Si vous attendez (ou désirez) quelque chose qui correspond à ce comportement:

 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  

Vous devez être conscient que si un certain nombre d’opérations “courtes” sont ajoutées à une queue, vous pouvez voir ce comportement à la place (car les opérations sont lancées dans le cadre de l’ajout à la queue):

 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  

Dans mon projet, j’avais besoin de savoir quand la dernière opération était terminée, après qu’un grand nombre d’opérations avaient été ajoutées à une série NSOperationQueue (c.-à-d. MaxConcurrentOperationCount = 1) et seulement lorsqu’elles étaient toutes terminées.

Googling J’ai trouvé cette déclaration d’un développeur Apple en réponse à la question “est un FIFO série NSoperationQueue?” –

Si toutes les opérations ont la même priorité (qui n’est pas modifiée après l’ajout de l’opération à une queue) et que toutes les opérations sont toujours – isReady == OUI au moment où elles sont placées dans la queue des opérations, une série NSOperationQueue est FIFO.

Chris Kane Cadres cocoa, Apple

Dans mon cas, il est possible de savoir quand la dernière opération a été ajoutée à la queue. Donc, après l’ajout de la dernière opération, j’ajoute une autre opération à la queue, de moindre priorité, qui ne fait rien mais envoie la notification que la queue a été vidée. Compte tenu de la déclaration d’Apple, cela garantit qu’un seul avis est envoyé uniquement après que toutes les opérations ont été effectuées.

Si des opérations sont ajoutées d’une manière qui ne permet pas de détecter la dernière (c’est-à-dire non déterministe), alors je pense que vous devez suivre les approches KVO mentionnées ci-dessus, avec une logique de garde supplémentaire pour essayer de détecter si des opérations peuvent être ajoutées.

🙂

Que diriez-vous d’append une NSOperation qui dépend de tous les autres afin qu’elle fonctionne en dernier?

Une alternative consiste à utiliser GCD. Reportez-vous à ceci comme référence.

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

C’est comme ça que je le fais.

Configurez la queue et enregistrez les modifications dans la propriété operations:

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

… et l’observateur (dans ce cas self ) implémente:

 - (void) observeValueForKeyPath:(NSSsortingng *) 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; } 

Dans cet exemple, “spinner” est un UIActivityIndicatorView montrant que quelque chose se passe. Évidemment, vous pouvez changer pour convenir …

Qu’en est-il de l’utilisation de KVO pour observer la propriété operationCount de la queue? Ensuite, vous en entendrez parler lorsque la queue aura été vidée, et aussi lorsqu’elle a cessé d’être vide. Traiter l’indicateur de progression peut être aussi simple que de faire quelque chose comme:

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

Ajouter la dernière opération comme:

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

Alors:

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

Avec ReactiveObjC, je trouve que cela fonctionne 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!"); } }]; 

FYI, vous pouvez y parvenir avec GCD dispatch_group dans swift 3 . Vous pouvez être averti lorsque toutes les tâches sont terminées.

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

Vous pouvez créer un nouveau NSThread ou exécuter un sélecteur en arrière-plan et y attendre. Lorsque NSOperationQueue termine, vous pouvez envoyer une notification vous-même.

Je pense à quelque chose comme:

 - (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 vous utilisez cette opération comme classe de base, vous pouvez transmettre le bloc whenEmpty {} à OperationQueue :

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

J’utilise une catégorie pour le faire.

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 

Utilisation :

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

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

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