95

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.

skaffman
  • 398,947
  • 96
  • 818
  • 769
Kornel
  • 97,764
  • 37
  • 219
  • 309

17 Answers17

167

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.

Nick Forge
  • 21,344
  • 7
  • 55
  • 78
  • One quick suggestion: Don't keep calling self.queue. It's needless overhead! Just use 'queue' unless assigning. I know it's just a quick demo block, so don't worry too much about it. :) – Sam Stewart Aug 30 '10 at 22:45
  • 26
    I would argue that you should use the property accessor, since it provides future-proofed encapsulation (if you decide e.g. to lazily-initialise the queue). Directly accessing a property by its ivar could be considered premature optimisation, but it really depends on the exact context. The time saved by directly accessing a property through its ivar is usually going to be negligible, unless you are referencing that property more than 100-1000 times a second (as an incredibly crude guesstimate). – Nick Forge Aug 31 '10 at 04:17
  • 2
    Tempted to downvote due to bad KVO usage. Proper usage described here: http://www.dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage/ – Nikolai Ruhe Jan 02 '13 at 19:03
  • 19
    @NikolaiRuhe You are correct - using this code when subclassing a class which itself uses KVO to observe `operationCount` on the same `NSOperationQueue` object would potentially lead to bugs, in which case you would need to use the context argument properly. It's unlikely to occur, but definitely possible. (Spelling out the actual problem is more helpful than adding snark + a link) – Nick Forge Jan 06 '13 at 23:56
  • 6
    Found an interesting idea [here](http://stackoverflow.com/questions/9998532/ios-how-to-know-when-nsoperationqueue-finish-processing-a-few-operations). I used that to subclass NSOperationQueue, added an NSOperation property, 'finalOpearation', that is set as a dependent of each operation added to the queue. Obviously had to override addOperation: to do so. Also added a protocol that sends a msg to a delegate when finalOperation completes. Has been working so far. – pnizzle May 06 '13 at 07:58
  • I *will* down vote for bad KVO usage. KVO is not a protocol to be taken lightly - to ensure safe usage requires a significant amount of due-diligence by the programmer. In this answer, there is no comment or code example to highlight the requirement to balance calls to `addObserver:` with a call to `removeObserver:`. – Austin Apr 04 '17 at 20:27
  • 1
    Much better! I'll be most happy when the options are specified, and the removeObserver: call is wrapped by a @try/@catch - It isn't ideal but the apple docs specify that there is no safety when calling removeObserver: ... if the object does not have an observer registration the application will crash. – Austin Apr 19 '17 at 17:37
20

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.

:)

software evolved
  • 4,314
  • 35
  • 45
  • Hi, do you know if and how is possible to be notified when each operation in the queue ends by using a NSOperationQueue with maxConcurrentOperationCount=1? – Sefran2 Jun 17 '11 at 09:37
  • @fran: I would have the operations post a notification upon completion. That way other modules can register as observers, and respond as each completes. If your @selector takes a notification object you can easily retrieve the object that posted the notification, in case you need further details about what op just completed. – software evolved Jun 19 '11 at 02:30
19

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

MostlyYes
  • 199
  • 1
  • 4
  • 1
    It might work, but it's a heavyweight solution, and it would be pain to manage if you need to add new tasks to the queue. – Kornel Sep 07 '09 at 23:00
  • this is actually very elegant and the one I preferred the most! you my vote. – Yariv Nissim Apr 26 '13 at 00:22
  • 1
    Personally this is my favorite solution. You can easily create a simple NSBlockOperation for the completion block that depends on all other operations. – Puneet Sethi Feb 22 '16 at 16:19
  • You might hit an issue that NSBlockOperation isn't called when the queue is cancelled. So you need to make your own operation that creates an error when cancelled and calls a block with an error param. – malhal Apr 18 '16 at 21:56
  • This is the best answer! – trapper Feb 23 '17 at 06:37
  • this is absolutely the best answer. – Trevis Thomas Jul 07 '17 at 18:34
  • This is what Apple suggests to developers. You can see in their WWDC videos, they never used KVO and also they explain how easy it is to use dependency among NSOperations. – Nitesh Borad Nov 17 '17 at 05:15
  • Actually this is not as good as it may seem at first because when you cancel all operations in a queue you may still want to run your completion. But in this case the last operation won't even start – Andrey Chernukha Feb 10 '21 at 12:30
12

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
});
nhisyam
  • 714
  • 7
  • 11
7

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

}

Caleb Lindsey
  • 91
  • 1
  • 4
  • where is `decrementOperationCount()` method invoked? – iksnae Jun 19 '20 at 00:45
  • @iksnae - I've updated my answer with a sublcass of **Operation**. I use **decrementOperationCount()** within the **didSet** of my **state** variable. Hope this helps! – Caleb Lindsey Jul 03 '20 at 17:42
  • This will only work if you set the maxConcurrentOperationCount to 1 because if they all occurring at the same time, the numberOfOperations can be theoretically miss the decrement inovkation since every operation occurring on a different thread the numberOfOperations variable is not thread safe. – XcodeNOOB Jan 31 '21 at 21:00
5

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...

Kris Jenkins
  • 4,083
  • 30
  • 40
  • 2
    That `for` loop seems potentially expensive (what if you cancel all operations at once? Wouldn't that get quadratic performance when queue is being cleaned up?) – Kornel Apr 18 '10 at 12:59
  • Nice one, but be careful with threads, because, according to the documentation: "...KVO notifications associated with an operation queue may occur in any thread." Probably, you'd need to move execution flow to the main operation queue before updating the spinner – Igor Vasilev Sep 15 '17 at 12:06
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

brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • Why is this a _completion_ ? A NSOperationQueue does not complete - it merely gets empty. The empty state can be entered several times during the lifetime of a NSOperationQueue. – CouchDeveloper Oct 12 '15 at 17:17
  • This doesn't work if the op1 and op2 finish before setCompletion is called. – malhal Apr 12 '16 at 19:29
  • Excellent answer, just 1 caveat that completion block gets called when queue is done with starting all the operation. Starting operations != operations are complete. – Saqib Saud Jul 10 '17 at 11:29
  • Hmm old answer, but I’d bet `waitUntilFinished` should be `YES` – brandonscript Jul 10 '17 at 14:59
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]; 

}
Stunner
  • 12,025
  • 12
  • 86
  • 145
pvllnspk
  • 5,667
  • 12
  • 59
  • 97
2

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)]
Sixten Otto
  • 14,816
  • 3
  • 48
  • 60
  • Did this work for you? In my application the `NSOperationQueue` from 3.1 complains that it is not KVO-compliant for the key `operationCount`. – zoul Oct 18 '09 at 06:37
  • I didn't actually try this solution in an app, no. Can't say whether the OP did. But the documentation clearly states that it *should* work. I'd file a bug report. http://developer.apple.com/iphone/library/documentation/Cocoa/Reference/NSOperationQueue_class/Reference/Reference.html – Sixten Otto Oct 18 '09 at 17:09
  • There is no operationCount property on NSOperationQueue in the iPhone SDK (at least not as of 3.1.3). You must have been looking at the Max OS X documentation page (http://developer.apple.com/Mac/library/documentation/Cocoa/Reference/NSOperationQueue_class/Reference/Reference.html) – Nick Forge Apr 17 '10 at 03:49
  • 1
    Time heals all wounds... and sometimes wrong answers. As of iOS 4, the `operationCount` property is present. – Sixten Otto Nov 03 '10 at 14:18
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!");
    }
}];
Stunner
  • 12,025
  • 12
  • 86
  • 145
1

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")
}
Abhijith
  • 3,094
  • 1
  • 33
  • 36
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
1

From iOS 13, you can use https://developer.apple.com/documentation/foundation/operationqueue/3172534-addbarrierblock

Invokes a block when the queue finishes all enqueued operations, and prevents subsequent operations from starting until the block has completed.

queue.addBarrierBlock {
    
}
onmyway133
  • 45,645
  • 31
  • 257
  • 263
0

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"];
}
pgb
  • 24,813
  • 12
  • 83
  • 113
  • It seems a bit silly to create thread just to put it to sleep. – Kornel Jun 26 '09 at 20:24
  • I agree. Still, I couldn't find another way around it. – pgb Jun 26 '09 at 20:49
  • How would you ensure that only one thread is waiting? I thought about flag, but that needs to be protected against race conditions, and I've ended up using too much NSLock for my taste. – Kornel Jun 26 '09 at 22:11
  • I think you can wrap the NSOperationQueue in some other object. Whenever you queue an NSOperation, you increment a number and launch a thread. Whenever a thread ends you decrement that number by one. I was thinking on a scenario where you could queue everything beforehand, and then start the queue, so you would need only one waiting thread. – pgb Jun 27 '09 at 13:55
0

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")
}
user1244109
  • 2,166
  • 2
  • 26
  • 24
  • 1
    Value of type 'OperationQueue' has no member 'whenEmpty' – Dale Jun 16 '18 at 13:53
  • @Dale if you click on the link, it will take you to a github page where everything is explained. If i recall correctly, the answer was written when Foundation's OperationQueue was still called NSOperationQueue; so there was less ambiguity maybe. – user1244109 Jun 17 '18 at 13:36
  • My bad...I made the false conclusion that the "OperationQueue" above was Swift 4's "OperationQueue". – Dale Jun 19 '18 at 17:28
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)
    }
}
kasyanov-ms
  • 431
  • 4
  • 12
0

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)")
})
afanaian
  • 1
  • 1