Im posting this question because I have seen a lot of confusion over this topic and I spent several hours debugging NSOperation subclasses as a result.
The problem i
I didn't read these answers in any great detail because these approaches are a) way too complicated and b) not using NSOperation the way it's designed to be used. You guys seem to be hacking functionality that already exists.
The solution is to subclass NSOperation and override the getter isConcurrent to return YES. You then implement the - (void)start method and begin your asynchronous task. You are then responsible for finishing it, meaning you have to generate KVO notifications on isFinished and isExecuting so that the NSOperationQueue can know the task is complete.
(UPDATE: Here's how you would subclass NSOperation) (UPDATE 2: Added how you would handle a NSRunLoop if you have code that requires one when working on a background thread. The Dropbox Core API for example)
// HSConcurrentOperation : NSOperation
#import "HSConcurrentOperation.h"
@interface HSConcurrentOperation()
{
@protected
BOOL _isExecuting;
BOOL _isFinished;
// if you need run loops (e.g. for libraries with delegate callbacks that require a run loop)
BOOL _requiresRunLoop;
NSTimer *_keepAliveTimer; // a NSRunLoop needs a source input or timer for its run method to do anything.
BOOL _stopRunLoop;
}
@end
@implementation HSConcurrentOperation
- (instancetype)init
{
self = [super init];
if (self) {
_isExecuting = NO;
_isFinished = NO;
}
return self;
}
- (BOOL)isConcurrent
{
return YES;
}
- (BOOL)isExecuting
{
return _isExecuting;
}
- (BOOL)isFinished
{
return _isFinished;
}
- (void)start
{
[self willChangeValueForKey:@"isExecuting"];
NSLog(@"BEGINNING: %@", self.description);
_isExecuting = YES;
[self didChangeValueForKey:@"isExecuting"];
_requiresRunLoop = YES; // depends on your situation.
if(_requiresRunLoop)
{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// run loops don't run if they don't have input sources or timers on them. So we add a timer that we never intend to fire and remove him later.
_keepAliveTimer = [NSTimer timerWithTimeInterval:CGFLOAT_MAX target:self selector:@selector(timeout:) userInfo:nil repeats:nil];
[runLoop addTimer:_keepAliveTimer forMode:NSDefaultRunLoopMode];
[self doWork];
NSTimeInterval updateInterval = 0.1f;
NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:updateInterval];
while (!_stopRunLoop && [runLoop runMode: NSDefaultRunLoopMode beforeDate:loopUntil])
{
loopUntil = [NSDate dateWithTimeIntervalSinceNow:updateInterval];
}
}
else
{
[self doWork];
}
}
- (void)timeout:(NSTimer*)timer
{
// this method should never get called.
[self finishDoingWork];
}
- (void)doWork
{
// do whatever stuff you need to do on a background thread.
// Make network calls, asynchronous stuff, call other methods, etc.
// and whenever the work is done, success or fail, whatever
// be sure to call finishDoingWork.
[self finishDoingWork];
}
- (void)finishDoingWork
{
if(_requiresRunLoop)
{
// this removes (presumably still the only) timer from the NSRunLoop
[_keepAliveTimer invalidate];
_keepAliveTimer = nil;
// and this will kill the while loop in the start method
_stopRunLoop = YES;
}
[self finish];
}
- (void)finish
{
// generate the KVO necessary for the queue to remove him
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
_isExecuting = NO;
_isFinished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end