alivingston
alivingston

Reputation: 1440

Chaining Completion Blocks

I have two instances of different classes who both need to add a completion block to a particular operation. I'll try to explain the problem generically rather than having to explain everything my app is attempting to do.

A view controller is calling into an instance of a resource manager class for it to save a resource. The resource manager then calls into the class of the resource to be saved to get a network operation for the save.

The instance of the resource creates the operation and gives it a completion block that will affect the state of the resource when it fires.

This is where my problem is - the resource class also needs to add a completion block to this operation in order for the view controller to be informed of the success or failure of the save.

Here's a snippit of the save method on the manager:

-(void)save:resource withCompletion:completion
{
.
.
.

NSOperation *operation = [resource operationForSave];

NSOperation __weak *weakOperation = operation;
void(^__weak resourceCompletion)(void)= operation.completionBlock;

[operation setCompletionBlock:^{
    if (resourceCompletion) {
        resourceCompletion();
        }

    if (completion) {
       if (weakOperation.error) {
            completion(NO, operation.error);
            }
        else {
            completion(YES, nil);
            }
        }
    }];

.
.
.
// add the operation to a network operation queue
}

While I think this will technically work, I'm not crazy about it. It feels pretty funky. I would prefer to have one block encapsulating the second block, but this isn't possible because the view controller and the resource are creating their own completion blocks, and the manager class is the one that has to smash them together.

Is there a more elegant way to chain these two completion blocks together in this situation, or is my current method of creating a block to contain the original two blocks the best I'm going to get?

Any input would be great appreciated.

Upvotes: 2

Views: 880

Answers (1)

rob mayoff
rob mayoff

Reputation: 385500

The code you posted will probably not work. When you replace the operation's completion block with your own block, you're probably removing the only strong reference to the original completion block (set by the resource). So your resourceCompletion variable, being weak, will become nil by the time setCompletionBlock: returns.

Just making resourceCompletion strong should fix the problem. But if you want to do it in a cleaner way, modify the operationForSave message (on the resource) to take a completion block itself:

__block NSNetworkOperation *operation = [resource operationForSaveWithCompletion:^{
    NSError *error = operation.error;
    completion(error == nil, error);

    // Break the retain cycle between this block and the operation object.
    operation = nil;
}];

And make it the job of the resource's own internal completion block to call the completion block you provide.

If you don't want to or can't modify the resource's API, you can still simplify your code by eliminating the weak references:

__block NSNetworkOperation *operation = [resource operationForSave];
__block void (^priorCompletion)(void) = operation.completionBlock;
operation.completionBlock = ^{
    if (priorCompletion) {
        priorCompletion);
        // Break possible retain cycle.
        priorCompletion = nil;
    }

    NSError *error = operation.error;
    completion(error == nil, error);
    // Break the retain cycle between this block and the operation object.
    operation = nil;
};

Also, I sincerely hope you don't really have a class named NSNetworkOperation, because Apple reserves the NS prefix (and all other two-letter prefixes) for its own use.

Upvotes: 2

Related Questions