views:

1623

answers:

5

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.

A: 

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
It seems a bit silly to create thread just to put it to sleep.
porneL
I agree. Still, I couldn't find another way around it.
pgb
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.
porneL
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
A: 

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

MostlyYes
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.
porneL
+2  A: 

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
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
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
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
+4  A: 

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
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?)
porneL
+7  A: 

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.

When you setup your queue, do this:

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

Then do this in your observeValueForKeyPath:

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


EDIT: iOS 4.0 now 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
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
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