[ACCEPTED]-Get notification when NSOperationQueue finishes all tasks-nsoperation

Accepted answer
Score: 167

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

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

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

When you 10 setup your queue, do this:

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

Then do this in 9 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 8 a property named queue)

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

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


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

Score: 20

If you are expecting (or desiring) something 34 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 33 aware that if a number of "short" operations 32 are being added to a queue you may see this 31 behavior instead (because operations are 30 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 29 my project I needed to know when the last 28 operation completed, after a large number 27 of operations had been added to a serial 26 NSOperationQueue (ie, maxConcurrentOperationCount=1) and 25 only when they had all completed.

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

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

Chris Kane Cocoa Frameworks, Apple

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

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

:)

Score: 19

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

Score: 12

One alternative is to use GCD. Refer to 1 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
});
Score: 7

As of iOS 13.0, the operationCount and operation properties are deprecated. It's 5 just as simple to keep track of the number 4 of operations in your queue yourself and 3 fire off a Notification when they've all completed. This 2 example works with an asynchronous subclassing 1 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.")
}

}

Score: 5

This is how I do it.

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

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

...and 4 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 3 this example "spinner" is a UIActivityIndicatorView showing that 2 something is happening. Obviously you can 1 change to suit...

Score: 4

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

0

Score: 2

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

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

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]; 

}

0

Score: 2

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

0

Score: 1

FYI,You can achieve this with GCD dispatch_group in swift 3. You 1 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")
}
Score: 1

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

0

Score: 0

You can create a new NSThread, or execute a selector 3 in background, and wait in there. When the 2 NSOperationQueue finishes, you can send a notification of 1 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"];
}
Score: 0

If you use this Operation as your base class, you 1 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")
}
Score: 0

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

0

Score: 0

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

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

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

More Related questions