I've been looking for a way to pass results for chained NSOperation. For example, lets assume we have 3 operations chained:
Operation1 to download JSON data from server
Operation2 to parse & model JSON received
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?
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.
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.
Related
I want try to understand block capture logic and now I have question about it. I have MeRequest and NSNumber properties.
#property (nonatomic) MeRequest *request;
#property (nonatomic) NSNumber *number;
Then, in viewDidLoad i call request method
self.request = [[MeRequest alloc] init];
[self.request meInfoSuccessBlock:^(NSDictionary *response) {
} failureBlock:^(Error *error) {
self.number = #5;
}];
- (void)meInfoSuccessBlock:(RequestSuccessBlock)success failureBlock:(RequestFailureBlock)failure {
self.method = #"GET";
self.parameters = #{};
[self performWithCompletion:^(id responseObject) {
NSDictionary *response = (NSDictionary *)responseObject;
if (success) {
success(response);
}
} onFailure:^(Error *error) {
if (failure) {
failure(error);
}
}];
}
- (AFHTTPRequestOperation *)performWithCompletion:(void(^)(id responseObject))completion
onFailure:(void(^)(Error *error))failure {
NSURLRequest *request = [[NetworkManager sharedManager] requestWithMethod:self.method path:self.path parameters:self.parameters];
if (_operation) {
[_operation cancel];
}
_operation = [[NetworkManager sharedManager] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
_operation = nil;
dispatch_semaphore_signal(_semaphore);
if (completion) {
completion(responseObject);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
_operation = nil;
dispatch_semaphore_signal(_semaphore);
if (failure) {
failure(_error);
}
}];
[_operation start];
return _operation;
}
And in failureBlock I set number to property. When I leave this controller I see dealloc message in console, that controller has been dealloced.
- (void)dealloc {
NSLog(#"%s", __PRETTY_FUNCTION__);
}
Why controller deallocs? I don't use weak reference to self
To know definitively, you'd have to post the implementation of the MeRequest class.
Without knowing that, this is an educated guess.
The blocks passed into self.request via meInfoSuccessBlock:failureBlock: may be nil'd out when the transaction is complete. That is, it may be something like:
- (void)meInfoSuccessBlock:... sB failureBlock:... fB {
_sB = sB; // put ref in ivar
_fB = fB; // because this is probably broken up across methods
dispatch_async(_queue, ^{
.... think hard ...
if (success) _sB(...);
else _fB(...);
_sB = nil;
_fB = nil;
};
}
So, first, you aren't creating a direct cyclic reference, but -- maybe -- a cyclic reference of self -> request -> _sB -> self. And, secondly, by assigning _sB = nil after computation is done and the callback is made, the cycle is broken.
Or, in your case, you have strong references to the blocks that only survive the scope. I.e. kinda like this:
- (void)meInfoSuccessBlock:... sB failureBlock:... fB {
dispatch_async(_queue, ^{
.... think hard ...
if (success) sB(...);
else fB(...);
// when the block finishes execution, fB and sB will be released
};
// when execution gets to here, the block above is the only strong references to sB and fB
}
That is, while you have a retain cycle, one reference in that cycle is explicitly tied to the lifespan of the callback blocks and since those only survive until the callback is complete, they get destroyed and that destroys the cycle.
I am trying to run AFURLConnectionOperation seen below on the currentQueue as I want to keep my main thread free for user interatoin, however nothing happens when I call mainQeue.
However if I call the same AFURLConnectionOperation on mainQueue it works perfectly.
Pleas see following code
// Send Batch
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:mutableOperations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
NSLog(#"%lu of %lu complete", (unsigned long)numberOfFinishedOperations, (unsigned long)totalNumberOfOperations);
} completionBlock:^(NSArray *operations) {
// check batch response
NSError *error;
for (AFHTTPRequestOperation *op in operations) {
if (op.isCancelled){
return ;
}
if (op.responseObject){
// Current JSON Batch complete
NSMutableArray *jsonObject = [NSJSONSerialization JSONObjectWithData:op.request.HTTPBody options:kNilOptions error:&error];
// Update sent_flag using current chunk
[[SyncModel sharedInstance] updateSentFlag:jsonObject];
}
if (op.error){
error = op.error;
NSLog(#"Error == %#", error);
}
}
}];
Then finally I call one or the other of the following code
[[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO]; // this works
[[NSOperationQueue currentQueue] addOperations:operations waitUntilFinished:NO]; // this dose not work
The reason is
You can use this method from within a running operation object to get a reference to the operation queue that started it. Calling this method from outside the context of a running operation typically results in nil being returned.
So,I guess,if you log [NSOperationQueue currentQueue],it is nil
If you want a new queue,use
[[NSOperationQueue alloc] init];
After adding the operation on queue, if the operation doesn't start eventually then there are two ways to get them executed.
Using wait block, for example during unit test using XCTest framework, use
XCTestExpectation *expectation1 = [self expectationWithDescription:#"ExtractColorsInternal function call on NSOperationQueue"];
dispatch_async(dispatch_get_main_queue(), ^{
[expectation1 fulfill];
});
[self waitForExpectationsWithTimeout:1000 handler:^(NSError *error) {
if (error != nil) {
NSLog(#"Error: %#", error.localizedDescription);
}
}];
call CFRunLoopRun(), which would execute the present operation in current queue succesfully
I'm having trouble with semaphore.
I have a serie of blocks and I want a block is executed just when the previous one has been finished its work.
I red that I have to play with gcd semaphore but the app stop working at the point signed in the code and it never enters in the block completation.
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSLog(#"1. AZIENDE: BEGIN");
[Model syncAziende:^(id response, NSError *error) {
dispatch_semaphore_signal(semaphore);
NSLog(#"2. AZIENDE: FINISH");
}];
/*BLOCKS HERE */dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(#"3. AZIENDE: BEGIN");
[Model syncContatti:^(id response, NSError *error) {
NSLog(#"4. AZIENDE: FINISH");
}];
Here's the output:
2014-03-26 09:35:56.561 NSalesCDC[1071:60b] 1. AZIENDE: BEGIN
Trying to use semaphores is not the correct approach to this.
Instead, chain your callbacks together. You can create your blocks outside of each other to prevent horrible, pyramid-like callback hell.
This should work for you:
// The block that is called when syncContatti: is complete
void (^contattiBlock)(id, NSError *) = ^(id response, NSError *error) {
NSLog(#"4. AZIENDE: FINISH");
};
// The block that is called when syncAziende: is complete
void (^aziendeBlock)(id, NSError *) = ^(id response, NSError *error) {
NSLog(#"2. AZIENDE: FINISH");
// Now, we know that syncAziende: is finished, we can start the next step
[Model syncContatti:conCattiBlock];
};
// Now we begin the entire chain of events
NSLog(#"1. AZIENDE: BEGIN");
[Model syncAziende:aziendeBlock];
One downside of this is that you have to define your blocks in reverse-order, but that's not too bad.
You can use dispatch_barrier_async(). dispatch_barrier_async() will wait until all the tasks that are scheduled before the barrier to finish execution and then it will start execution. All the tasks scheduled after the barrier will wait for the barrier to finish.
dispatch_async(myQueue,
// this will start working now
});
dispatch_barrier_async(myQueue,
// this will wait until the previous block finish
//and will block the next one from execution
})
dispatch_async(myQueue,
// this will wait for the barrier to finish
});
Use it this way:
- (void) testSomethingAPI
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[Model syncAziende: ^(id response, NSError *error)
{
// Your Stuff here...
dispatch_semaphore_signal(semaphore);
}];
while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW))
{
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.f]];
}
}
You may use NSOperation dependencies.
E.g.
NSOperationQueue * que = [[NSOperationQueue alloc] init];
NSBlockOperation * op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"first");
}];
NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"second");
}];
[op2 addDependency:op];
[que addOperations:#[op,op2] waitUntilFinished:NO];
You can also call the second block within the first or use other guys approaches
If your reply to my comment above really is the structure of your code, it cries out for refactoring. The repetition is a good candidate for abstraction.
Perhaps something like:
static const struct {
SEL selector;
NSString* message;
} steps[] = {
{ #selector(syncAziende:), #"Sincrinizzo i contatti" }.
{ #selector(syncContatti:), #"Sincrinizzo le destinazioni" }.
// ...
};
- (void) doStep:(int) step
{
if (step < sizeof(steps) / sizeof(steps[0]))
{
[Model performSelector:steps[step].selector withObject:[^(id response, NSError *error){
hud.labelText = [NSString stringWithFormat:#"%d/%d: %#", step + 1, sizeof(steps) / sizeof(steps[0]), steps[step].message];
[self doStep:step + 1];
} copy]];
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
hud.mode = MBProgressHUDModeText;
hud.labelText = #"Sincronizzazione terminata";
[hud hide:YES afterDelay:1.5];
});
}
}
...
[self doStep:0];
I have some networking code with heavy JSON parsing going on. It needs to be done in the background to not block the main thread. The code looks like this :
-(void) getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
// sometimes I have more requests
// startOperations is a wrapper on AFHTTPClient enqueueBatchOfHTTPRequestOperations:progressBlock:completionBlock:
// that handles errors and loading views
[self startOperations:#[op] completionBlock:^(NSArray *operations) {
// getBgQueue = return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(getBgQueue(), ^{
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
[self.localContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
// this is executed on main thread
if(completion) completion(...);
}];
});
}];
}
(AFNetworking 1.x)
The above code works very fine, but it's a pain to setup and write. And often the whole method content is wrapped inside another block to fetch some required data first... basically the blocks just pile up and makes ugly code
I'm using enqueueBatchOfHTTPRequestOperations and not individual completion blocks on AFJSONRequestOperation because batch completion block would sometimes fire before all individual operations completion blocks... (I also read somewhere that Mattt discouraged doing this)
Any pointers on how to do better than this?
I'm not sure what you want here, but just like "longcat is long", it's somewhat inherent in the pattern: 'continuation-passing style is continuation-passing style'. If you want to flatten things out a bit, you could make local block variables, but to a certain degree, you're stuck because you need the completion for -MR_saveToPersistentStoreWithCompletion to close over data in order to pass it to the -getSomeDataWithParameters... completion, but data won't exist until the -startOperations completion is executed.
You could probably achieve a less-nested appearance by using a bunch of __block variables, and splitting the code into several local blocks, but to me that feels kind of like cutting off your nose to spite your face. This code is readily understandable the way it is.
By the way... I notice that you're closing over op in the -startOperations completion block. This is fine because you're enqueuing op by doing -startOperations: #[op] ... but it would arguably be cleaner to get op from the operations parameter to the completion. I tightened this up as much as seemed reasonable:
- (void)getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...;
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
[self startOperations:#[op] completionBlock:^(NSArray *operations) {
for (AFJSONRequestOperation *op in operations) {
dispatch_async(getBgQueue(), ^{
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
void (^mrSaveCompletion)(BOOL, NSError*) = completion ? ^(BOOL success, NSError *error) { completion(data); } : nil;
[self.localContext MR_saveToPersistentStoreWithCompletion: mrSaveCompletion];
});
}
}];
}
This will fan out each response potentially to a different thread. If you want all responses to execute on a single background thread, just swap the nesting of the for loop and the dispatch_async.
From there, the only really "superfluous" code is the dispatch_async. You could eliminate that by making -startOperations:... take a queue parameter where you would pass in the queue you wanted the completion to be called. Maybe like this:
- (void)startOperations: (NSArray*)ops completionQueue: (dispatch_queue_t)queue completionBlock: (void (^)(NSArray*))completion
{
void (^completionWrapper)(NSArray*) = !completion ? nil : ^(NSArray* ops) {
if (queue)
dispatch_async(queue, ^{ completion(ops); });
else
completion(ops);
};
[self startOperations: ops completionBlock: completionWrapper];
}
- (void)getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...;
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
[self startOperations:#[op] completionQueue: getBgQueue() completionBlock:^(NSArray *operations) {
for (AFJSONRequestOperation *op in operations) {
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
void (^mrSaveCompletion)(BOOL, NSError*) = !completion ? nil : ^(BOOL success, NSError *error) { completion(data); };
[self.localContext MR_saveToPersistentStoreWithCompletion: mrSaveCompletion];
});
}];
}
I am downloading four plist files asynchronously over the internet. I need to wait until all four files are downloaded, until I either on the first run, push a UIViewController, or on all subsequent runs, refresh the data, and reload all my UITableViews.
On the first run, everything works perfectly. When refreshing though, all four url requests are called, and started, but never call their completion or failure blocks, and the UI freezes. Which is odd since I preform all operations in a background thread. I have not been able to figure out why this is happening.
The first load and the refresh methods call the four "update" methods in the same way, and use NSCondition in the same way.
For the first run:
- (void)loadContentForProgram:(NSString *)programPath
{
NSLog(#"Start Load Program");
AppDelegate *myDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
hud = [[MBProgressHUD alloc] initWithView:myDelegate.window];
[myDelegate.window addSubview:hud];
hud.labelText = #"Loading...";
hud.detailsLabelText = #"Loading Data";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//Do stuff here to load data from files
//Update From online files
hud.detailsLabelText = #"Updating Live Data";
resultLock = NO;
progressLock = NO;
recallLock = NO;
stageLock = NO;
condition = [[NSCondition alloc] init];
[condition lock];
[self updateCurrentCompsText];
[self updateCompetitionResults];
[self updateCompetitionRecalls];
[self updateCompetitionProgress];
while (!resultLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!stageLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!recallLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!progressLock) {
[condition wait];
}
NSLog(#"Unlock");
[condition unlock];
updateInProgress = NO;
//Reset Refresh controls and table views
self.refreshControlsArray = [[NSMutableArray alloc] init];
self.tableViewsArray = [[NSMutableArray alloc] init];
NSLog(#"Finished Loading Program");
[[NSNotificationCenter defaultCenter] postNotificationName:#"WMSOFinishedLoadingProgramData" object:nil]; //Pushes view controller
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:myDelegate.window animated:YES];
});
});
}
When refreshing data:
- (void)updateProgramContent
{
if (!updateInProgress) {
updateInProgress = YES;
for (int i = 0; i < self.refreshControlsArray.count; i++) {
if (!((UIRefreshControl *)self.refreshControlsArray[i]).refreshing) {
[self.refreshControlsArray[i] beginRefreshing];
[self.tableViewsArray[i] setContentOffset:CGPointMake(0.0, 0.0) animated:YES];
}
}
resultLock = NO;
stageLock = NO;
recallLock = NO;
progressLock = NO;
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
condition = [[NSCondition alloc] init];
[condition lock];
[self updateCompetitionProgress];
[self updateCompetitionRecalls];
[self updateCompetitionResults];
[self updateCurrentCompsText];
while (!resultLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!stageLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!recallLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!progressLock) {
[condition wait];
}
NSLog(#"Unlock");
[condition unlock];
});
for (int i = 0; i < self.refreshControlsArray.count; i++) {
[self.refreshControlsArray[i] performSelector:#selector(endRefreshing) withObject:nil afterDelay:1.0];
[self.tableViewsArray[i] performSelector:#selector(reloadData) withObject:nil afterDelay:1.0];
}
updateInProgress = NO;
}
}
The block below that appears in each loading method above, corresponds to a method that will download and update a specific piece of data.
[self updateCompetitionProgress];
[self updateCompetitionRecalls];
[self updateCompetitionResults];
[self updateCurrentCompsText];
which runs:
- (void)updateCompetitionResults
{
__block NSDictionary *competitionResultsData = nil;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"Some URL",[self.programName stringByReplacingOccurrencesOfString:#" " withString:#"%20"]]] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:20.0];
AFPropertyListRequestOperation *operation = [AFPropertyListRequestOperation propertyListRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id propertyList) {
competitionResultsData = (NSDictionary *)propertyList;
[competitionResultsData writeToFile:[#"SOME LOCAL PATH"] atomically:NO];
[self updateCompetitionResultsWithDictionary:competitionResultsData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id propertyList) {
competitionResultsData = [NSDictionary dictionaryWithContentsOfFile:[#"SOME LOCAL PATH"]];
NSLog(#"Failed to retreive competition results: %#", error);
[self updateCompetitionResultsWithDictionary:competitionResultsData];
}];
[operation start];
}
and the completion and failure blocks call the same method to update the data
- (void)updateCompetitionResultsWithDictionary:(NSDictionary *)competitionResultsData
{
//Do Stuff with the data here
resultLock = YES;
[condition signal];
}
So, Why does this work on the first run, but not any of the subsequent runs?
As I mentioned in my comments, above, the most obvious problem is that you're invoking methods that use condition before you initialize condition. Make sure initialize condition before you start calling updateCompetitionResults, etc.
In terms of a more radical change, I might suggest retiring NSCondition altogether, and use operation queues:
I might use NSOperationQueue (or you can use dispatch groups, too, if you want, but I like the operation queue's ability to configure how many concurrent operations you can operate ... also if you get to a point that you want to cancel operations, I think NSOperationQueue offers some nice features there, too). You can then define each download and processing as a separate NSOperation (each of the downloads should happen synchronously, because they're running in an operation queue, you get the benefits of asynchronous operations, but you can kick off the post-processing immediately after the download is done). You then just queue them up to run asynchronously, but define a final operation which is dependent upon the other four will kick off as soon as the four downloads are done. (By the way, I use NSBlockOperation which provides block-functionality for NSOperation objects, but you can do it any way you want.)
And whereas your updateProgramContent might download asynchronously, it processes the four downloaded files sequentially, one after another. Thus, if the first download takes a while to download, it will hold up the post-processing of the others. Instead, I like to encapsulate both the downloading and the post processing of each of the four plist files in a single NSOperation, each. Thus, we enjoy maximal concurrency of not only the downloading, but the post-processing, too.
Rather than using the AFNetworking (which I'm generally a big fan of) plist-related method, I might be inclined to use NSDictionary and NSArray features that allow you to download a plist from the web and load them into the appropriate structure. These dictionaryWithContentsOfURL and arrayWithContentsOfURL run synchronously, but because we're doing this in a background operation, everything runs asynchronously like you want. This also bypasses the saving them to files. If you wanted them saved to files in your Documents directory, you can do that easily, too. Clearly, if you're doing something sophisticated in your downloading of the plist files (e.g. your server is engaging in some challenge-response authentication), you can't use the convenient NSDictionary and NSArray methods. But if you don't need all of that, the simple NSDictionary and NSArray methods, ___WithContentsOfURL make life pretty simple.
Pulling this all together, it might look like:
#interface ViewController ()
#property (nonatomic, strong) NSArray *competitions;
#property (nonatomic, strong) NSDictionary *competitionResults;
#property (nonatomic, strong) NSDictionary *competitionRecalls;
#property (nonatomic, strong) NSDictionary *competitionProgress;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self transfer];
}
- (void)allTransfersComplete
{
BOOL success;
if (self.competitions == nil)
{
success = FALSE;
NSLog(#"Unable to download competitions");
}
if (self.competitionResults == nil)
{
success = FALSE;
NSLog(#"Unable to download results");
}
if (self.competitionRecalls == nil)
{
success = FALSE;
NSLog(#"Unable to download recalls");
}
if (self.competitionProgress == nil)
{
success = FALSE;
NSLog(#"Unable to download progress");
}
if (success)
{
NSLog(#"all done successfully");
}
else
{
NSLog(#"one or more failed");
}
}
- (void)transfer
{
NSURL *baseUrl = [NSURL URLWithString:#"http://insert.your.base.url.here/competitions"];
NSURL *competitionsUrl = [baseUrl URLByAppendingPathComponent:#"competitions.plist"];
NSURL *competitionResultsUrl = [baseUrl URLByAppendingPathComponent:#"competitionresults.plist"];
NSURL *competitionRecallsUrl = [baseUrl URLByAppendingPathComponent:#"competitionrecalls.plist"];
NSURL *competitionProgressUrl = [baseUrl URLByAppendingPathComponent:#"competitionprogress.plist"];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4; // if your server doesn't like four concurrent requests, you can ratchet this back to whatever you want
// create operation that will be called when we're all done
NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
// any stuff that can be done in background should be done here
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// any user interface stuff should be done here; I've just put this in a separate method so this method doesn't get too unwieldy
[self allTransfersComplete];
}];
}];
// a variable that we'll use as we create our four download/process operations
NSBlockOperation *operation;
// create competitions operation
operation = [NSBlockOperation blockOperationWithBlock:^{
// download the competitions and load it into the ivar
//
// note, if you *really* want to download this to a file, you can
// do that when the download is done
self.competitions = [NSArray arrayWithContentsOfURL:competitionsUrl];
// if you wanted to do any post-processing of the download
// you could do it here.
NSLog(#"competitions = %#", self.competitions);
}];
[completionOperation addDependency:operation];
// create results operation
operation = [NSBlockOperation blockOperationWithBlock:^{
self.competitionResults = [NSDictionary dictionaryWithContentsOfURL:competitionResultsUrl];
NSLog(#"competitionResults = %#", self.competitionResults);
}];
[completionOperation addDependency:operation];
// create recalls operation
operation = [NSBlockOperation blockOperationWithBlock:^{
self.competitionRecalls = [NSDictionary dictionaryWithContentsOfURL:competitionRecallsUrl];
NSLog(#"competitionRecalls = %#", self.competitionRecalls);
}];
[completionOperation addDependency:operation];
// create progress operation
operation = [NSBlockOperation blockOperationWithBlock:^{
self.competitionProgress = [NSDictionary dictionaryWithContentsOfURL:competitionProgressUrl];
NSLog(#"competitionProgress = %#", self.competitionProgress);
}];
[completionOperation addDependency:operation];
// queue the completion operation (which is dependent upon the other four)
[queue addOperation:completionOperation];
// now queue the four download and processing operations
[queue addOperations:completionOperation.dependencies waitUntilFinished:NO];
}
#end
Now, I don't know which of your plist's are arrays and which are dictionaries (in my example, I made competitions an array and the rest were dictionaries keyed by the competition id), but hopefully you get the idea of what I was shooting for. Maximize concurrency, eliminate NSCondition logic, really make the most of NSOperationQueue, etc.
This may be all to much to take in, but I only mention it as an alternative to NSCondition. If your current technique works, that's great. But the above outlines how I would tackle a challenge like this.