4

I've been looking for a way to pass results for chained NSOperation. For example, lets assume we have 3 operations chained:

  1. Operation1 to download JSON data from server
  2. Operation2 to parse & model JSON received
  3. Operation3 to download user images

So Op3 would be dependent on Op2, which is dependent on Op1. But I'm looking for way to pass results from Op1 -> Op2, then from Op2 -> Op3 as:

[operation1 startWithURL:url];
[operation2 parseJSONfromOp1IntoModel:JSONData];
[operation3 downloadUserImagesForUser: UserModelObject];

and nesting blocks doesn't seem to be a clean readable solution, any idea?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Haitham
  • 83
  • 4
  • did you try using `completionBlock`? – kientux Nov 10 '15 at 15:51
  • 2
    have you seen https://developer.apple.com/videos/play/wwdc2015-226/ – Wain Nov 10 '15 at 15:59
  • I found https://github.com/berzniz/Sequencer a useful library for doing this sort of sequencing. – Graham Perks Nov 10 '15 at 16:31
  • Sadly, nesting blocks with custom completion handlers is typical `NSOperation` approach. Or look at promises/futures, e.g. [PromiseKit](https://github.com/mxcl/PromiseKit) or [RSPromise](https://github.com/couchdeveloper/RXPromise). – Rob Nov 10 '15 at 16:44
  • @Rob , thanks but i have to stick to SDK and not 3rd party libraries for some reasons, otherwise i would have done it in less than 8 ReactiveCocoa lines :) – Haitham Nov 10 '15 at 16:49
  • @Wain , yes but it's not typically chaining,more of 1 operation at a time then `updateUI` , operations are not dependant on previous results. – Haitham Nov 10 '15 at 16:51
  • @GrahamPerks thanks for the Tip – Haitham Nov 10 '15 at 16:51
  • Very well covered by the WWDC 2015 on NSOperation (and the associated sample code) – matt Nov 10 '15 at 17:07

2 Answers2

4

If you want to chain operations, but don't like the nesting, you can use NSOperation subclasses, and then define your own completion handlers:

DownloadOperation *downloadOperation = [[DownloadOperation alloc] initWithURL:url];
ParseOperation *parseOperation = [[ParseOperation alloc] init];
DownloadImagesOperation *downloadImagesOperation = [[DownloadImagesOperation alloc] init];

downloadOperation.downloadCompletionHandler = ^(NSData *data, NSError *error) {
    if (error != nil) {
        NSLog(@"%@", error);
        return;
    }

    parseOperation.data = data;
    [queue addOperation:parseOperation];
};

parseOperation.parseCompletionHandler = ^(NSDictionary *dictionary, NSError *error) {
    if (error != nil) {
        NSLog(@"%@", error);
        return;
    }

    NSArray *images = ...;

    downloadImagesOperation.images = images;
    [queue addOperation:downloadImagesOperation];
};

[queue addOperation:downloadOperation];

Frankly, though, I'm not sure that's any more intuitive than the nested approach:

DownloadOperation *downloadOperation = [[DownloadOperation alloc] initWithURL:url downloadCompletionHandler:^(NSData *data, NSError *error) {
    if (error != nil) {
        NSLog(@"%@", error);
        return;
    }

    ParseOperation *parseOperation = [[ParseOperation alloc] initWithURL:data parseCompletionHandler:^(NSDictionary *dictionary, NSError *error) {
        if (error != nil) {
            NSLog(@"%@", error);
            return;
        }

        NSArray *images = ...

        DownloadImagesOperation *downloadImagesOperation = [[DownloadImagesOperation alloc] initWithImages:images imageDownloadCompletionHandler:^(NSError *error) {
            if (error != nil) {
                NSLog(@"%@", error);
                return;
            }

            // everything OK
        }];
        [queue addOperation:downloadImagesOperation];
    }];
    [queue addOperation:parseOperation];
}];
[queue addOperation:downloadOperation];

By the way, the above assumes that you're familiar with subclassing NSOperation, especially the subtleties of creating an asynchronous NSOperation subclass (and doing all of the necessary KVO). If you need examples of how that's done, let me know.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I was going to say the first way looks correct but it has a small mistake. After each operation is configured, dependencies should be set up, and then all 3 operations should be added to the queue, not after each ends. If any errors are returned then cancel the queue. – malhal Jan 08 '17 at 17:02
  • Yep, that's another pattern. I deliberately omitted it because it's a bit fragile. When you have operation dependencies, that doesn't necessarily apply their completion handlers, which could run asynchronously on different thread. So, if you're passing data in completion handlers, your `NSOperation` subclasses must call their completion handlers _synchronously_ before the operation finishes. But, it's questionable design to have code integrity contingent upon implementation details of another class. The above options are solely reliant upon public interfaces which is more robust. – Rob Jan 08 '17 at 17:55
2

Creating chained operations:

Create the Op2 from within the completion block of Op1, then use delegation or something similar to set the dependency on the newly created operation. You can use this pattern to chain as many as you want. To pass the result in the completion block, you cannot use completionBlock that is on NSOperation. You will need to define your own (like I did with almostFinished) in order to pass the result through.

- (void)someMethod {
    Operation1 *operation1 = [[Operation1 alloc] init];
    operation1.almostFinished = ^(id op1Result) {

        Operation2 *operation2 = [[Operation2 alloc] initWithResultFromOp1: op1Result];
        operation2.almostFinished = ^(id op2Result) {

            Operation3 *operation3 = [[Operation3 alloc] initWithResultFromOp2:op2Result];
            operation3.completionBlock = ^{
                NSLog(@"Operations 1 and 2 waited on me, but now we're all finished!!!);
            };

            [operation2 addDependency:operation3];
            [queue addOperation:operation3];
        };

        [operation1 addDependency:operation2];
        [queue addOperation:operation2];
    };

    [queue addOperation:operation1];
}

Custom Subclass

You will need to subclass NSOperation for this to work. As I mentioned, you need to define your own completion block AND make sure that completion block is called before the operation is truly finished so that you can add the dependency. Instead of adding the dependency in the new completion block, you could add it in a different block or delegate method. This way kept my example concise.

@interface Operation: NSOperation {
@property (nonatomic, copy) void (^almostFinished)(id result);
@end

@implementation Operation {
    //...

- (void)main {
    //...
    // Call here to allow to add dependencies and new ops
    self.almostFinished(result);  

    // Finish the op
    [self willChangeValueForKey:@"isFinished"];
    // repeat for isExecuting and do whatever else
    [self didChangeValueForKey:@"isFinished"];
}
@end

EDIT: This isn't the most readable thing, but it contains all the code in one method. If you want to get fancy, then place things out in delegate methods or get creative with how you define these things.

keithbhunter
  • 12,258
  • 4
  • 33
  • 58