2
votes

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.

1
Hi. Why do you not use the completionBlock property of the NSOperation? It will be automaticaly invoked after how operation will be finished. Before adding an operation to the queue, just set completionBlock.Serge Maslyakov
The completion block of a NSBlockOperation is to notify that an operation is completed. But if the code to be executed has its own completion block, this will be called immediately, since your code is async, right?loretoparisi
I'm understand your issue. I solved this by using dispatch_semaphore_t. See this link for example https://gist.github.com/Mozilla9/16b0b5013256ff89e52bSerge Maslyakov
@Mozilla That's correct, please post the answer so I can accept it!. I was trying the same solution using the dispatch_group_notify and dispatch_group_async, but the only way is the dispatch_semaphore_t +1. Thank you.loretoparisi

1 Answers

4
votes

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];
        });
}