views:

760

answers:

2

Hi

I am using a subclass of NSOperation to do some background processes. I want the operation to be cancelled when the user clicks a button.

Here's what my NSOperation subclass looks like

- (id)init{
    self = [super init];
    if(self){
     //initialization code goes here
     _isFinished = NO;
     _isExecuting = NO;
    }

    return self;
}

- (void)start
{
    if (![NSThread isMainThread])
    {
        [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
        return;
    }
    [self willChangeValueForKey:@"isExecuting"];
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];

    //operation goes here
}

- (void)finish{
    //releasing objects here

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

- (void)cancel{
    [self willChangeValueForKey:@"isCancelled"];

    [self didChangeValueForKey:@"isCancelled"];
    [self finish];
}

And this is how I am adding objects of this class to a queue and listening for KVO notifications

operationQueue = [[NSOperationQueue alloc] init]; [operationQueue setMaxConcurrentOperationCount:5]; [operationQueue addObserver:self forKeyPath:@"operations" options:0 context:&OperationsChangedContext];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == &OperationsChangedContext) {
        NSLog(@"Queue size: %u", [[operationQueue operations] count]);
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

To cancel an operation (on a button click for instance), I tried calling -cancel but it doesn't make a difference. Also tried calling -finish but even that doesn't change anything.

Every time I add an operation to the queue, the queue size only increases. finish is called (checked using NSLog statements) but it doesn't really end the operation. I'm still not very confident I'm doing this right

Can someone please tell me where I am going wrong?

Thanks a lot

+1  A: 

From the NSOperation Class Reference:

Cancelling an operation does not immediately force it to stop what it is doing. Although respecting the value returned by the isCancelled is expected of all operations, your code must explicitly check the value returned by this method and abort as needed.

i.e. the cancel method does not actually cancel the operation unless your implementation enforces this. Again, from the relevant section:

- (void)cancel

This method does not force your operation code to stop. Instead, it updates the object’s internal flags to reflect the change in state.

jnic
Yes read that. But where do I check for isCancelled? You can't fire a timer inside an NSOperation!
lostInTransit
Surely you wouldn't have to poll for the flag change, you'd simply perform your abort procedure from the cancel method when it gets called?
jnic
Please bear with me here. I am calling an AVAudioPlayer's play method in the NSOperation class. In the cancel method I stop the player, but what next? The number of operations in the queue still show up as the same.
lostInTransit
I thought that setting isFinished to true should automatically dequeue the operation, but you appear to be doing this already, so perhaps something else is going on here? It's worth stepping through in the debugger and inspecting the values to be sure.
jnic
A: 

You just need the following to achieve it:

In your NSOperation subclass, add to main method

 while (! self.isCancelled) {
  [NSThread sleepForTimeInterval:1];
 }

In the GUI class, you need an instance variable to your NSOperation subclass, and in the method that manages the button, you cancel your NSOperation subclass. For instance:

- (IBAction) clickButton: (UIBarButtonItem *) item{
 [myOperation cancel]; 
}
G Mauri