Search code examples
iosnsoperationnsoperationqueue

NSBlockOperation with Nested Completion Block


Supposed to have a method with completion block to execute:

[container insert:data
               completion:^(NSDictionary *result, NSError *error) {

               }];

I need to make this concurrent using NSOperation (more than GCD dispatch block, since I need more control over operation flow and cancellation).

Now, assumed to execute a normal completion block I could use NSBlockOperation like

- (NSOperation *)executeBlock:(void (^)(void))block
                    inQueue:(NSOperationQueue *)queue
                    completion:(void (^)(BOOL finished))completion
{
    NSOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:block];
    NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
        completion(blockOperation.isFinished);
    }];

    [completionOperation addDependency:blockOperation];

    [[NSOperationQueue currentQueue] addOperation:completionOperation];
    [queue addOperation:blockOperation];

    return blockOperation;
}

and so calling it like

[self executeBlock:^{
        /// my sync code

    }   inQueue:operationQueue
        completion:^(BOOL finished) {

    }];

The problem instead having async code there:

void (^completionBlock)() = ^void() {
        // this is the NSOperation completion block where sync code is executed
    };

and

    void (^insertCompletionBlock)(NSDictionary *, NSError *) = ^void(NSDictionary *result, NSError *error) {
      // this is the insert api completion block 
   };

So having

[container insert:data completion:insertCompletionBlock];

If I do a nested call like

[self executeBlock:^{
    [container insert:data
           completion:^(NSDictionary *result, NSError *error) {

           }];

}   inQueue:operationQueue
    completion:^(BOOL finished) {

}];

this NSOperation will end immediately, since the insert:completion: method will return after its call having a completion block.

So, how to serialize this execution in order to have a NSBlockOperation called after the nested completion block of insert:completion: is executed?

[UPDATED] Using the solution by @Mozilla I came out with a custom NSBlockOperation that I used to add some properties on:

@interface MyCloudOperation: NSBlockOperation
@property(nonatomic,strong) id result;
@property(nonatomic,strong) NSError *error;
@end
@implementation MXMCloudOperation
@end

and this

MyCloudOperation *blockOp=[[MyCloudOperation alloc] init];
    __weak MXMCloudOperation *weakBlockOp=blockOp;
    [blockOp setCompletionBlock:^{
        if(completion) completion(weakBlockOp.result,weakBlockOp.error);
    }];
    [blockOp addExecutionBlock:^{
        dispatch_semaphore_t mutex = dispatch_semaphore_create(0);
        void (^insertCompletionBlock)(NSDictionary *, NSError *) = ^void(NSDictionary *result, NSError *error) {
            if(error) {
                weakBlockOp.error=error;
                NSLog(@"Error saving to %@ data\n%@", containerName,
                      error.localizedDescription);
            } else {
                weakBlockOp.result=result;
                NSLog(@"Data %@ sent", result);
            }
            dispatch_semaphore_signal(mutex);
        };
        [container insert:data completion:insertCompletionBlock];
        dispatch_semaphore_wait(mutex, DISPATCH_TIME_FOREVER);
    }];
    [operationQueue addOperation:blockOp];

What I don't like here is to reference my NSBlockOperation to pass the completion handler's parameters, but I didn't find out a better solution right now.


Solution

  • I solved this by using dispatch_semaphore_t.

    - (void)saveWebDataInternal:(ResponseModel *)data completion:(void(^)(NSArray *))completion
    {
            NSBlockOperation *op = [[NSBlockOperation alloc] init];
    
            op.completionBlock = ^{
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self loadCachedDataInternal:completion];
                });
            };
    
            [op addExecutionBlock:^{
                dispatch_semaphore_t mutex = dispatch_semaphore_create(0);
    
                [self.cacheDAO asyncImport:data completion:^{
                    dispatch_semaphore_signal(mutex);
                }];
    
                dispatch_semaphore_wait(mutex, DISPATCH_TIME_FOREVER);
            }];
    
            // start operation
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                [op start];
            });
    }