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.
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
I encountered this error while implementing an asynchronous subclass of NSOperation
.
The Swift way of referencing key paths is with the #keyPath
directive, so I was doing this (_executing
and _finished
are my internal variables):
self.willChangeValue(forKey: #keyPath(Operation.isExecuting))
self._executing = false
self.didChangeValue(forKey: #keyPath(Operation.isExecuting))
self.willChangeValue(forKey: #keyPath(Operation.isFinished))
self._finished = true
self.didChangeValue(forKey: #keyPath(Operation.isFinished))
Unfortunately, the #keyPath
expressions above resolve to "executing"
and "finished"
, respectively, and we need to throw KVO notifications for "isExecuting"
and "isFinished"
. This is why the completionBlock
is not getting invoked.
The solution is to hard code them as such:
self.willChangeValue(forKey: "isExecuting")
self._executing = false
self.didChangeValue(forKey: "isExecuting")
self.willChangeValue(forKey: "isFinished")
self._finished = true
self.didChangeValue(forKey: "isFinished")