I've got a bunch of NSOperations
added to a NSOperationQueue
. The operation queue has the maxConcurrentOperationCount
set to 1, so that the NSOperations
run one after the other.
Now, in the completionBlock
of a NSOperation
I want to cancel all pending NSOperations
by calling cancelAllOperations
on the NSOperationQueue
.
Is it safe to do this? Can I be sure that the start
-method of the next operation is called only after the completionBlock
of the previous operation has been fully executed? Or do the completionBlock
of the previous operation and the task of the current operation run concurrently?
The reason why I'm asking: I use AFNetworking to execute a batch of AFHTTPRequestOperations
and want to perform one request only if all previous requests of the batch were successful.
My findings below no longer seem to be true. I've re-run the tests on iOS 8 and iOS 9 and the completion block of an operation always runs concurrently with the next operation. Currently, I don't see a way to make an operation wait for the previous completion block to finish.
I just tried this scenario in a sample project. Here is the result:
If the NSOperationQueue
's maxConcurrentOperationCount
is set to 1, an NSOperation
's completionBlock
and the next NSOperation
in the queue run simultaneously.
But, if every NSOperation
is linked to its previous operation by calling addDependency:
, the execution of an operation waits until the previous operation's completionBlock
has finished.
So, if you want to cancel the next operation in the completionBlock
of the current operation and be sure that it is cancelled before it is started, you have to set dependencies between the NSOperations
by calling addDependency:
NSOperation establishes dependency only based on the completion states of operations, and not on the results of completed operations.
However, most of the scenarios that I encounter are such that, the execution of operations depend not only on the completion of some other operations, but also based on the results obtained from the completed operations.
I ended up doing like the below method, but still exploring if there is a better way:
1) Operation-A runs
2) Operation-A compeletes and its completionBlock runs
3) In the OperationA's completion block, check for the result obtained from Operation-A.
- If result is X, create Operation-B and add to the queue.
- If result is Y, create Operation-C and add to the queue.
- If result is error, create Operation-D (usually an alert operation) and add to the queue
So, this ends up as a sequence of operations, that are dynamically added to the queue, depending on the result of completed operations.
I came up with another seemingly better way to ensure that an operaion is executed only if certain conditions (based on the results of previously finished operations) are met, else, the operation is cancelled.
One important consideration here is that the condition check for running an operation should not be coded inside the operation subclass, thus allowing the operation subclass to be poratble across different scenarios and apps.
Solution:
- Have a condition block property inside the subclass, and set whatever condition form where the operation is instantiated.
- Override "isReady" getter of the NSOperation subclass, check the condition there, and thus determine if its ready for execution.
- If [super isReady] is YES, which means the dependent operations are all finished, then evaluate the necessary condition.
- If the condition check is passed, return YES. Else, set isCancelled to YES and return YES for isReady
Code:
In the interface file have the block property:
typedef BOOL(^ConditionBlock)(void);
@property (copy) ConditionBlock conditionBlock;
In the implementation, override isReady, and cancelled:
@implementation ConditionalOperation
- (BOOL)isReady {
if([super isReady]) {
if(self.conditionBlock) {
if(!self.conditionBlock()) {
[self setCancelled:YES];
}
return YES;
} else {
return YES;
}
} else {
return NO;
}
}