Get notification when NSOperationQueue finishes all tasks

NSOperationQueue has waitUntilAllOperationsAreFinished, but I don't want to wait synchronously for it. I just want to hide progress indicator in UI when queue finishes.

What's the best way to accomplish this?

I can't send notifications from my NSOperations, because I don't know which one is going to be last, and [queue operations] might not be empty yet (or worse - repopulated) when notification is received.

51696 次浏览

You can create a new NSThread, or execute a selector in background, and wait in there. When the NSOperationQueue finishes, you can send a notification of your own.

I'm thinking on something like:

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


...


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

How about adding an NSOperation that is dependent on all others so it will run last?

What about using KVO to observe the operationCount property of the queue? Then you'd hear about it when the queue went to empty, and also when it stopped being empty. Dealing with the progress indicator might be as simple as just doing something like:

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

This is how I do it.

Set up the queue, and register for changes in the operations property:

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

...and the observer (in this case self) implements:

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

In this example "spinner" is a UIActivityIndicatorView showing that something is happening. Obviously you can change to suit...

Use KVO to observe the operations property of your queue, then you can tell if your queue has completed by checking for [queue.operations count] == 0.

Somewhere in the file you're doing the KVO in, declare a context for KVO like this (more info):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

When you setup your queue, do this:

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

Then do this in your 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];
}
}

(This is assuming that your NSOperationQueue is in a property named queue)

At some point before your object fully deallocs (or when it stops caring about the queue state), you'll need to unregister from KVO like this:

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


Addendum: iOS 4.0 has an NSOperationQueue.operationCount property, which according to the docs is KVO compliant. This answer will still work in iOS 4.0 however, so it's still useful for backwards compatibility.

If you are expecting (or desiring) something that matches this behavior:

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>

You should be aware that if a number of "short" operations are being added to a queue you may see this behavior instead (because operations are started as part of being added to the queue):

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>

In my project I needed to know when the last operation completed, after a large number of operations had been added to a serial NSOperationQueue (ie, maxConcurrentOperationCount=1) and only when they had all completed.

Googling I found this statement from an Apple developer in response to the question "is a serial NSoperationQueue FIFO?" --

If all operations have the same priority (which is not changed after the operation is added to a queue) and all operations are always - isReady==YES by the time they get put in the operation queue, then a serial NSOperationQueue is FIFO.

Chris Kane Cocoa Frameworks, Apple

In my case it is possible to know when the last operation was added to the queue. So after the last operation is added, I add another operation to the queue, of lower priority, which does nothing but send the notification that the queue had been emptied. Given Apple's statement, this ensures that only a single notice is sent only after all operations have been completed.

If operations are being added in a manner which doesn't allow detecting the last one, (ie, non-deterministic) then I think you have to go with the KVO approaches mentioned above, with additional guard logic added to try to detect if further operations may be added.

:)

Add the last operation like:

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

So:

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


}

One alternative is to use GCD. Refer to this as reference.

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

I'm using a category to do this.

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

Usage:

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

With ReactiveObjC I find this works nicely:

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

If you use this Operation as your base class, you could pass whenEmpty {} block to the OperationQueue:

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


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


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

FYI,You can achieve this with GCD dispatch_group in swift 3. You can get notified when all tasks are finished.

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

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

If you got here looking for a solution with combine - I ended up just listening to my own state object.

@Published var state: OperationState = .ready
var sub: Any?


sub = self.$state.sink(receiveValue: { (state) in
print("state updated: \(state)")
})

As of iOS 13.0, the operationCount and operation properties are deprecated. It's just as simple to keep track of the number of operations in your queue yourself and fire off a Notification when they've all completed. This example works with an asynchronous subclassing of Operation too.

class MyOperationQueue: OperationQueue {
            

public var numberOfOperations: Int = 0 {
didSet {
if numberOfOperations == 0 {
print("All operations completed.")
                

NotificationCenter.default.post(name: .init("OperationsCompleted"), object: nil)
}
}
}
    

public var isEmpty: Bool {
return numberOfOperations == 0
}
    

override func addOperation(_ op: Operation) {
super.addOperation(op)
        

numberOfOperations += 1
}
    

override func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool) {
super.addOperations(ops, waitUntilFinished: wait)
        

numberOfOperations += ops.count
}
    

public func decrementOperationCount() {
numberOfOperations -= 1
}
}

Below is a subclass of Operation for easy asynchronous operations

class AsyncOperation: Operation {
    

let queue: MyOperationQueue


enum State: String {
case Ready, Executing, Finished
    

fileprivate var keyPath: String {
return "is" + rawValue
}
}


var state = State.Ready {
willSet {
willChangeValue(forKey: newValue.keyPath)
willChangeValue(forKey: state.keyPath)
}
    

didSet {
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: state.keyPath)
        

if state == .Finished {
queue.decrementOperationCount()
}
}
}


override var isReady: Bool {
return super.isReady && state == .Ready
}


override var isExecuting: Bool {
return state == .Executing
}


override var isFinished: Bool {
return state == .Finished
}


override var isAsynchronous: Bool {
return true
}


public init(queue: MyOperationQueue) {
self.queue = queue
super.init()
}


override func start() {
if isCancelled {
state = .Finished
return
}
    

main()
state = .Executing
}


override func cancel() {
state = .Finished
}


override func main() {
fatalError("Subclasses must override main without calling super.")
}

}


let queue = OperationQueue()
queue.underlyingQueue = .global(qos: .background)
queue.progress.totalUnitCount = 3
queue.isSuspended = true


queue.addOperation(blockOperation1)
queue.addOperation(blockOperation2)
queue.addOperation(blockOperation3)


/// add at end if any operation is added after addBarrierBlock then that operation will wait unit BarrierBlock is finished


queue.addBarrierBlock {
print("All operations are finished \(queue.progress.fractionCompleted) - \(queue.progress.completedUnitCount)" )
}




queue.isSuspended = false