Better asynchronous control flow with Objective-C blocks - objective-c

I'm using AFNetworking for asynchronous calls to a web service. Some of these calls must be chained together, where the results of call A are used by call B which are used by call C, etc.
AFNetworking handles results of async calls with success/failure blocks set at the time the operation is created:
NSURL *url = [NSURL URLWithString:#"http://api.twitter.com/1/statuses/public_timeline.json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSLog(#"Public Timeline: %#", JSON);
} failure:nil];
[operation start];
This results in nested async call blocks which quickly becomes unreadable. It's even more complicated when tasks are not dependent on one another and instead must execute in parallel and execution depends on the results of all operations.
It seems that a better approach would be to leverage a promises framework to clean up the control flow.
I've come across MAFuture but can't figure out how best to integrate it with AFNetworking. Since the async calls could have multiple results (success/failure) and don't have a return value it doesn't seem like an ideal fit.
Any pointers or ideas would be appreciated.

I created a light-weight solution for this. It's called Sequencer and it's up on github.
It makes chaining API calls (or any other async code) easy and straightforward.
Here's an example of using AFNetworking with it:
Sequencer *sequencer = [[Sequencer alloc] init];
[sequencer enqueueStep:^(id result, SequencerCompletion completion) {
NSURL *url = [NSURL URLWithString:#"https://alpha-api.app.net/stream/0/posts/stream/global"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
completion(JSON);
} failure:nil];
[operation start];
}];
[sequencer enqueueStep:^(NSDictionary *feed, SequencerCompletion completion) {
NSArray *data = [feed objectForKey:#"data"];
NSDictionary *lastFeedItem = [data lastObject];
NSString *cononicalURL = [lastFeedItem objectForKey:#"canonical_url"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:cononicalURL]];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
completion(responseObject);
} failure:nil];
[operation start];
}];
[sequencer enqueueStep:^(NSData *htmlData, SequencerCompletion completion) {
NSString *html = [[NSString alloc] initWithData:htmlData encoding:NSUTF8StringEncoding];
NSLog(#"HTML Page: %#", html);
completion(nil);
}];
[sequencer run];

I haven't used it yet, but it sounds like Reactive Cocoa was designed to do just what you describe.

It was not uncommon when using AFNetworking in Gowalla to have calls chained together in success blocks.
My advice would be to factor the network requests and serializations as best you can into class methods in your model. Then, for requests that need to make sub-requets, you can call those methods in the success block.
Also, in case you aren't using it already, AFHTTPClient greatly simplifies these kinds of complex network interactions.

PromiseKit could be useful. It seems to be one of the more popular promise implementations, and others have written categories to integrate it with libraries like AFNetworking, see PromiseKit-AFNetworking.

There is an Objective-C implementation of CommonJS-style promises here on Github:
https://github.com/mproberts/objc-promise
Example (taken from the Readme.md)
Deferred *russell = [Deferred deferred];
Promise *promise = [russell promise];
[promise then:^(NSString *hairType){
NSLog(#"The present King of France is %#!", hairType);
}];
[russell resolve:#"bald"];
// The present King of France is bald!
I haven't yet tried out this library, but it looks 'promising' despite this slightly underwhelming example. (sorry, I couldn't resist).

You can combine NSBlockOperation with semaphore to achieve it:
- (void)loadDataByOrderSuccess:(void (^)(void))success failure:(void (^)(void))failure {
// first,load data1
NSBlockOperation * operation1 = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self loadData1Success:^{
dispatch_semaphore_signal(sema);
} failure:^{
!failure ?: failure();
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}];
// then,load data2
NSBlockOperation * operation2 = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self loadData2Success:^{
dispatch_semaphore_signal(sema);
} failure:^{
!failure ?: failure();
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}];
// finally,load data3
NSBlockOperation * operation3 = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self loadData3Success:^{
dispatch_semaphore_signal(sema);
} failure:^{
!failure ?: failure();
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
!success ?: success();
}];
[operation2 addDependency:operation1];
[operation3 addDependency:operation2];
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
[queue addOperations:#[operation1, operation2, operation3] waitUntilFinished:NO];
}

Related

Having problems using AFNetworking fetching JSON

So i recently wanted to change from using NSUrlConnection to AFNetworking. I can receive the JSON data with both methods buts when using with AFNetworking something weird happens.
This is how it looks like with NSURLConnection
and this is how it looks like with AFNetworking
I have no idea what that (struct __lidb_autoregen_nspair) is and i dont know if that is the thing that is preventing me from displaying the data
This is the code from AFNetworking, i use the sample code from ray
-(void) fetchData{
// 1
NSURL *url = [NSURL URLWithString:string];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 2
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
operation.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"text/html"];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// 3
jsonDict = (NSMutableDictionary *)responseObject;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// 4
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Error Retrieving Weather"
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[alertView show];
}];
// 5
[operation start];
}
----------------------------------------------------------------------------------------- Edit
-(NSMutableDictionary *) getAllGames{
[self fetchData];
DataParser *dataParserObjec = [[DataParser alloc] init];
return [dataParserObjec sendBackAllGames:jsonDict];
}
You are setting the acceptableContentTypes to text/html. I presume you are doing that because your web-service is not setting the correct Content-Type header to indicate that it's application/json. If you fixed the web service to provide the correct header Content-Type, you could then remove this acceptableContentTypes line in your Objective-C code.
If you're wondering why you didn't have to worry about that with NSURLConnection, that's because NSURLConnection doesn't do any validation of Content-Type (unless, of course, you write your own code in, for example, didReceiveResponse, that checked this).
You suggest that you are unable to display the data. But yet there it is, in your second screen snapshot. I personally would be less worried about internal representation than whether I could access the data from the NSDictionary. If you
NSLog(#"responseObject=%#", responseObject);
at #3, inside the success block, what precisely do you see? I'd wager you'll see your NSDictionary fine (despite the subtle differences in the internal representation).
My contention is that you are getting the data back successfully. Yes, your web service should set the correct Content-Type header so you don't have to overwrite the acceptableContentTypes value, but it looks like AFNetworking is retrieving your data fine.
The likely issue is that your main thread is trying to use jsonDict before the asynchronous network request is done. So the trick is to defer the use of the jsonDict until the asynchronous request is done.
You've updated your question showing us that you're instantiating a DataParser and calling sendBackAllGames. You should put that code inside the completion block of your asynchronous network request.
Alternatively, you could use a completion block pattern in your fetchData method, and then the getAllGames method could supply the sendBackAllGames code in a completion block that fetchData calls inside the success block of the AFHTTPRequestOperation.
If you used the completion block pattern, it would look like
-(void) fetchDataWithCompletionHandler:(void (^)(id responseObject, NSError *error))completionHandler {
NSURL *url = [NSURL URLWithString:string];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
// removed once we fixed the `Content-Type` header on server
//
// operation.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"text/html"];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
if (completionHandler) {
completionHandler(responseObject, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[[[UIAlertView alloc] initWithTitle:#"Error Retrieving Weather"
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil] show];
if (completionHandler) {
completionHandler(nil, error);
}
}];
[operation start];
}
And you'd call it like so:
[self fetchDataWithCompletionHandler:^(id responseObject, NSError *error) {
if (responseObject) {
DataParser *dataParserObjec = [[DataParser alloc] init];
NSMutableDictionary *results = [dataParserObjec sendBackAllGames:jsonDict];
// do whatever you want with results
}
}];
If the method that called getAllGames needed the data to be returned, you'd repeat this completion block pattern for this method, too.

AFJsonRequest and Unit-Testing needs for synchronous request,

I'm trying to do some unit-testing on my code.
I'm facing an issue, unit-testing fonctions aren't waiting for the asynchronous request to be finished. I'd like to wait for it only if i'm testing it, and not when i run it.
I figured out how to check for this :
if([[[[NSProcessInfo processInfo] environment] objectForKey:#"TARGET"] isEqualToString:#"TEST"])
With the respective environnement variables set in my project.
But i can't find a proper way to wait for it. I tryed with a semaphore, and that works, but i'm wandering if there's any other way which would be easier, and reader-friendly.
EDIT :
Finally, i did it like this : I guess i could have do something better, but that works and looks fair enough to me.
static int statusCode;
- (void)requestContent:(NSString*)urlString
{
statusCode = 0;
NSLog(#"Request Content");
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
[self ParseGoogleImageSearchResponse:JSON];
statusCode = 1;
NSLog(#"Done");
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON)
{
NSLog(#"%#", [error userInfo]);
statusCode = 2;
}];
[operation start];
if([[[[NSProcessInfo processInfo] environment] objectForKey:#"TARGET"] isEqualToString:#"TEST"])
{
while(statusCode == 0)
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
NSLog(#"WAITING");
}
}
}
One way to accomplish async unit tests for AFNetworking calls is to use GHUnit.
GHUnit handles spinning the runloop and waiting for async operations to finish automatically, so your unit test code can be much more readable.
Here is a basic tutorial that covers using AFNetworking, GHUnit, and CocoaPods all together.
Here is a basic test written using AFNetworking and GHUnit.
- (void)test
{
[self prepare];
__block NSError *responseError = nil;
__block id resposeObject = nil;
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
resposeObject = JSON;
[self notify:kGHUnitWaitStatusSuccess forSelector:_cmd];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON)
{
responseError = error
[self notify:kGHUnitWaitStatusSuccess forSelector:_cmd];
}];
[operation start];
// Wait for the async activity to complete
[self waitForStatus:kGHUnitWaitStatusSuccess timeout:kNetworkTimeout];
// Check Error
GHAssertNil(responseError, #"");
GHAssertNotNil(resposeObservation, #"Response Observation is nil");
// Validate data...
}
Hope that helps!

AFNetworking updating URL in request

Is there any easy way to update a request's URL after it was initiated in AFNetworking?
Let's say my app is downloading a 2GB in chunks, we'll have 200 chunks 10MB each. For each I'm creating a AFHTTPRequestOperation and add it to the queue.
Problem is, that the server from which I'm downloading has a timeout on all URLs, which means I'll get 403 after that time and I have to generate a new one. I need to do it before the time runs out.
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
{
NSURL *fileURL = [[NSURL alloc] initWithString:[downloadFile filePath]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
NSLog(#"Normal range: %lli-%lli", [downloadChunk startingByte], [downloadChunk endingByte]);
NSString *range = [NSString stringWithFormat:#"bytes=%lli-%lli", newStartingByte, [downloadChunk endingByte]];
[request setValue:range forHTTPHeaderField:#"Range"];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", [NSString stringWithFormat:#"Chunk complete: %#.%i", [downloadFile fileName], [downloadChunk chunkId]]);
if (download.downloadedBytes == download.size)
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation setDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
download.downloadedBytes += bytesRead;
}];
[queue addOperation:operation];
}
}
No, it's not possible to modify a scheduled operation. Generally it's a good thing, meaning that you can schedule the operation and be absolutely sure that it's going to be executed based on original conditions.
I see two options in your case. First, you can schedule operations one by one, and verify if the link is valid, updating it if required.
Second, you can react to 403 Forbidden by updating the link and re-scheduling same operation.

Delay when updating view in the completionHandler of an async HTTP request

In my app when the user presses a button I start a HTTP asynchronous request (using [NSURLConnection sendAsynchronousRequest...]) and change the text of UILabel in the completionHandler block. This change, however, does not take place when the request is concluded and instead happens around 2-3 seconds later. Below is a code snippet that results in this behavior.
- (IBAction)requestStuff:(id)sender
{
NSURL *url = [NSURL URLWithString:#"http://stackoverflow.com/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
exampleLabel.text = [NSString stringWithFormat:#"%d", httpResponse.statusCode];
}];
}
A similar behavior happens when I attempt to create an UIAlertView inside the completionHandler.
- (IBAction)requestStuff:(id)sender
{
NSURL *url = [NSURL URLWithString:#"http://stackoverflow.com/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if ([httpResponse statusCode] == 200) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"It worked!"
message:nil
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}
}];
}
A small difference, though, is that the screen dims when [alert show] is executed. The alert itself only appears 2-3 seconds later like in the previous scenario.
I'm guessing this is related to how the UI is handled by the app's threads, but I'm not sure. Any guidance on why the delay happens will be greatly appreciated.
According to The Apple Docs.
Threads and Your User Interface
If your application has a graphical user interface, it is recommended that you receive user-related events and initiate interface updates from your application’s main thread. This approach helps avoid synchronization issues associated with handling user events and drawing window content. Some frameworks, such as Cocoa, generally require this behavior, but even for those that do not, keeping this behavior on the main thread has the advantage of simplifying the logic for managing your user interface.
Calling your UI updates on the main thread would solve this problem. Surround your UI code with a call to the main thread (below).
dispatch_async(dispatch_get_main_queue(), ^{
exampleLabel.text = [NSString stringWithFormat:#"%d", httpResponse.statusCode];
});
There are other ways to do calls on the main thread, but using the simpler GCD commands would do the job. Again, see the Threaded Programming Guide for more info.
This could happen because all UI stuff should be called in a main queue. Try this:
- (IBAction)requestStuff:(id)sender
{
NSURL *url = [NSURL URLWithString:#"http://stackoverflow.com/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
dispatch_async(dispatch_get_main_queue(), ^{
exampleLabel.text = [NSString stringWithFormat:#"%d", httpResponse.statusCode];
});
}];
}
You can try to create a method that sets the text, and inside the block you call:
[self performSelectorOnMainThread:#selector(mySelector) withObject:nil waitUntilDone:NO];
The selector will call and executed on main thread. Hope this help....

Waiting for completion block to complete in an AFNetworking request

I am making a JSON request with AFNetworking and then call [operation waitUntilFinished] to wait on the operation and the success or failure blocks. But, it seems to fall right though - in terms of the log messages, I get "0", "3", "1" instead of "0", "1", "3"
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"http://google.com"]];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
httpClient.parameterEncoding = AFFormURLParameterEncoding;
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:#"query", #"q", nil];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:[url path] parameters:params];
NSLog(#"0");
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *innerRequest, NSHTTPURLResponse *response, id JSON) {
NSLog(#"1");
gotResponse = YES;
} failure:^(NSURLRequest *innerRequest, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"2");
gotResponse = YES;
}];
NSLog(#"Starting request");
[operation start];
[operation waitUntilFinished];
NSLog(#"3");
This works by using AFNetworking to set up the requests, but making a synchronous call then handling the completion blocks manually. Very simple. AFNetworking doesn't seem to support this https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-FAQ, though the work around is simple enough.
#import "SimpleClient.h"
#import "AFHTTPClient.h"
#import "AFJSONRequestOperation.h"
#import "AFJSONUtilities.h"
#implementation SimpleClient
+ (void) makeRequestTo:(NSString *) urlStr
parameters:(NSDictionary *) params
successCallback:(void (^)(id jsonResponse)) successCallback
errorCallback:(void (^)(NSError * error, NSString *errorMsg)) errorCallback {
NSURLResponse *response = nil;
NSError *error = nil;
NSURL *url = [NSURL URLWithString:urlStr];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
httpClient.parameterEncoding = AFFormURLParameterEncoding;
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:[url path] parameters:params];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if(error) {
errorCallback(error, nil);
} else {
id JSON = AFJSONDecode(data, &error);
successCallback(JSON);
}
}
#end
That should (almost) work. Your call to
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:[url path] parameters:params];
should probably not pass [url path] to the path: parameter. In AFNetworking land, that path is everything after the base url (for example the base url could be "http://google.com" and the path "/gmail" or whatever).
That being said, it's probably not a good idea to make the asynchronous operation into a thread-blocking synchronous operation with waitUntilFinished, but I'm sure you have your reasons... ;)
I just had the same problem and found a different solution. I had two operations that depend on each other, but can load in parallel. However, the completion block of the second operation can not be executed before the completion block of the first one has finished.
As Colin pointed out, it might be a bad choice to make a web request block. This was essential to me, so I did it asynchronously.
This is my solution:
// This is our lock
#interface SomeController () {
NSLock *_dataLock;
}
#end
#implementation
// This is just an example, you might as well trigger both operations in separate
// places if you get the locking right
// This might be called e.g. in awakeFromNib
- (void)someStartpoint {
AFJSONRequestOperation *operation1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:[NSURLRequest requestWithURL:url1]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id data) {
// We're done, we unlock so the next operation can continue its
// completion block
[_dataLock unlock];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id data) {
// The request has failed, so we need to unlock for the next try
[_dataLock unlock];
}];
AFJSONRequestOperation *operation2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:[NSURLRequest requestWithURL:url2]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id data) {
// The completion block (or at least the blocking part must be run in a
// separate thread
[NSThread detachNewThreadSelector:#selector(completionBlockOfOperation2:) toTarget:self withObject:data];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id data) {
// This second operation may fail without affecting the lock
}];
// We need to lock before both operations are started
[_dataLock lock];
// Order does not really matter here
[operation2 start];
[operation1 start];
}
- (void)completionBlockOfOperation2:(id)data {
// We wait for the first operation to finish its completion block
[_dataLock lock];
// It's done, so we can continue
// We need to unlock afterwards, so a next call to one of the operations
// wouldn't deadlock
[_dataLock unlock];
}
#end
Use Delegate method call
Put the method inside block which will call itself when downloading/uploading completes.