Why my completionBlock never gets called in an NSOperation?

ios6, iphone, nsoperation

Solution

You've kind of fallen into a weird space between concurrent and non-concurrent `NSOperation` subclasses here. Typically, when you implement `main`, your operation is non-concurrent, and `isFinished` changes to `YES` as soon as `main` exits.

However, you've provided your own implementation of `isFinished`, and also coded it so that `isFinished` doesn't return `YES` until after `main` has exited. This makes your operation start to act like a concurrent operation in many ways - at least including the need to manually emit KVO notifications.

The quick solution to your problem is to implement `setFinished:` using `(will|did)ChangeValueForKey:` calls. (I also changed the ivar name to reflect naming prevailing naming conventions). Below is an `NSOperation` subclass that I believe accurately models your operation's workings, in terms of finishing in a concurrent fashion.

@implementation TestOperation {
    BOOL _isFinished;
}

- (void)setFinished:(BOOL)isFinished
{
    [self willChangeValueForKey:@"isFinished"];
    // Instance variable has the underscore prefix rather than the local
    _isFinished = isFinished;
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isFinished
{
    return ([self isCancelled] ? YES : _isFinished);
}

- (void)main
{
    NSLog(@"%@ is in main.",self);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self setFinished:YES];
    });
}

@end

I'm not familiar with your requirements so perhaps you have a pressing need, but your operation would seem a more natural fit for a concurrent operation that uses `start` instead of `main`. I've implemented a small example that appears to be working correctly.

@implementation TestOperation {
    BOOL _isFinished;
    BOOL _isExecuting;
}

- (void)setFinished:(BOOL)isFinished
{
    if (isFinished != _isFinished) {
        [self willChangeValueForKey:@"isFinished"];
        // Instance variable has the underscore prefix rather than the local
        _isFinished = isFinished;
        [self didChangeValueForKey:@"isFinished"];
    }
}

- (BOOL)isFinished
{
    return _isFinished || [self isCancelled];
}

- (void)cancel
{
    [super cancel];
    if ([self isExecuting]) {
        [self setExecuting:NO];
        [self setFinished:YES];
    }
}

- (void)setExecuting:(BOOL)isExecuting {
    if (isExecuting != _isExecuting) {
        [self willChangeValueForKey:@"isExecuting"];
        _isExecuting = isExecuting;
        [self didChangeValueForKey:@"isExecuting"];
    }
}

- (BOOL)isExecuting
{
    return _isExecuting;
}

- (void)start
{
    NSLog(@"%@ is in start. isCancelled = %@", self, [self isCancelled] ? @"YES" : @"NO");
    if (![self isCancelled]) {
        [self setFinished:NO];
        [self setExecuting:YES];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
            sleep(1);
            [self setExecuting:NO];
            [self setFinished:YES];
        });
    }
}
@end

Problem

I've sublcassed an NSOperation and set my completionBlock but it seems to never enter even when the operation finishes. Here's my code: A catalog controller class sets up the NSOperation: ``` - (void)setupOperation { ... ImportWordOperation *importWordOperation = [[ImportWordOperation alloc] initWithCatalog:words]; [importWordOperation setMainObjectContext:[app managedObjectContext]]; [importWordOperation setCompletionBlock:^{ [(ViewController *)[[app window] rootViewController] fetchResults]; }]; [[NSOperationQueue mainQueue] addOperation:importWordOperation]; [importWordOperation release]; ... } ``` As you can see, I'm setting the completion block to execute a method on the main thread, in some other controller. Then, in `main` my subclassed NSOperation class: `ImportWordOperation.m`, I fire-up the background operation. I even overrode `isFinished` iVar in order for the completion method to be triggered: ``` - (void)setFinished:(BOOL)_finished { finished = _finished; } - (BOOL)isFinished { return (self.isCancelled ? YES: finished); } - (void)addWords:(NSDictionary *)userInfo { NSError *error = nil; AppDelegate *app = [AppDelegate sharedInstance]; NSManagedObjectContext *localMOC = [userInfo valueForKey:@"localMOC"]; NSEntityDescription *ent = [NSEntityDescription entityForName:@"Word" inManagedObjectContext:localMOC]; for (NSDictionary *dictWord in [userInfo objectForKey:@"words"]) { Word *wordN = [[Word alloc] initWithEntity:ent insertIntoManagedObjectContext:localMOC]; [wordN setValuesForKeysWithDictionary:dictWord]; [wordN release]; } if (![[userInfo valueForKey:@"localMOC"] save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } [localMOC reset]; [self setFinished:YES]; } - (void)main { finished = NO; NSManagedObjectContext *localMOC = nil; NSUInteger type = NSConfinementConcurrencyType; localMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:type]; [localMOC setUndoManager:nil]; [localMOC setParentContext:[self mainObjectContext]]; if (![self isCancelled]) { if ([self.words count] > 0) { [self performSelectorInBackground:@selector(addWords:) withObject:@{@"words":self.words, @"localMOC":localMOC}]; } } } ``` If I remove isFinished accessor methods, then the completion block gets called but way before `ImportWordOperation` finishes. I've read code that I've found that uses its own completion block but then what's the use for the completion block in NSOperation subclasses anyway? Any ideas or point to a similar solved situation would be greatly appreciated.

Original source