Objective-C GCD wait for Block to finish (AFNetworking) - iphone

I've been trying to experiment with some code from a tutorial, however not having much success due to not getting my head around GCD.
I have an class named API.m and here is the code regarding GCD:
+ (API *) sharedInstance
{
static API *sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
sharedInstance = [[self alloc] initWithBaseURL:[NSURL URLWithString:APIHost]];
});
return sharedInstance;
}
-(void)commandWithParams:(NSMutableDictionary*)params
onCompletion:(JSONResponseBlock)completionBlock
{
NSMutableURLRequest *apiRequest = [self multipartFormRequestWithMethod:#"POST"
path:APIPath
parameters:params
constructingBodyWithBlock: ^(id <AFMultipartFormData>formData) {
//TODO: attach file if needed
}];
AFJSONRequestOperation* operation = [[AFJSONRequestOperation alloc] initWithRequest: apiRequest];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//success!
completionBlock(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//failure :(
completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription] forKey:#"error"]);
}];
[operation start];
}
I make a simple test by implementing a button and getting an NSArray to print it's content to the output window:
- (IBAction)test:(id)sender {
NSMutableDictionary* params =[NSMutableDictionary dictionaryWithObjectsAndKeys:
#"pending", #"command",
[[[API sharedInstance] user] objectForKey:#"UserID"] , #"userID",
nil];
[[API sharedInstance] commandWithParams:params
onCompletion:^(NSDictionary *json) {
//result returned
if ([json objectForKey:#"error"]==nil) {
// Simple example
[self.users addObject:#"1"];
} else {
//error
[UIAlertView title:#"Error" withMessage:[json objectForKey:#"error"]];
}
}];
NSLog(#"%#", self.users);
}
Now when I first click the button an empty NSArray is printed to the output window, but when I press it again it print's "1". It's clear that the program is reaching NSLog before the completion block has time to fully execute. Could someone please help me modify the code so that I have the option to have the NSLog execute after the completion block has finished?

Not sure as to what you are trying to accomplish, but if the goal is to just have NSLog execute after the completion block, you can move the NSLog statement after
[self.users addObject:#"1"];
If you have some code which you want to execute after adding it to the array, you can have
[self methodName]; in the completion block and it will get called there.
Completion block, is the code which is run after execution of the code which you wanted run. The code which you wanted run, will happen asynchronously and on another thread. After that code is run, the completion block code will get executed.

Related

AFNetworking gives incorrect progress count

I'm using AFHTTPRequestOperation to download a file. But on pausing and resuming the operation, the api gives incorrect progress count. I am downloading data using following code
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:str]];
_downloadOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
_downloadOperation.outputStream = [NSOutputStream outputStreamToFileAtPath:strFilePath append:YES];
[_downloadOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSLog(#"Progress %lld",totalBytesRead * 100 / totalBytesExpectedToRead);
}];
[_downloadOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"downloaded %#",operation.request.URL);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"failed");
}];
[_downloadOperation start];
I pause the operation when user goes in background,
- (void)applicationDidEnterBackground:(UIApplication *)application
{
if ([objAPI.downloadOperation isExecuting])
[objAPI.downloadOperation pause];
}
and resume operation when user comes in foreground
- (void)applicationWillEnterForeground:(UIApplication *)application
{
if ([objAPI.downloadOperation isPaused])
[objAPI.downloadOperation resume];
}
Example: If the operation is paused at progress 20% , on resuming it starts from 20% but ends at 120% . In other words the progress count goes incorrectly after pausing the operation.
Kindly help me to solve the problem
There is a related issue which would cause the percentages when using AFURLSessionManager to be incorrect.
The pull-request at https://github.com/AFNetworking/AFNetworking/pull/1786 may fix this issue.

NSURLConnection sendAsynchronousRequest: Blocking Main Thread

I'm using NSURLConnection to make multiple asynchronous requests. I'd like to show a progress indicator to show how many requests have been completed out of the total number to be performed. However, when I attempt to set up and display this progress indicator either before making the request, or in another method called before performing the request, it will not show. The progress indicator displays fine when the request is commented out. But when it's not, it's as if Xcode looks ahead and sees an asynchronous request coming and blocks the main thread, thereby making UI changes impossible.
Here's the relevant code being called, including both the request and code to show the progress indicator:
- (void)getRegionalInformationFromChecked:(NSSet *)set atIndex:(NSInteger)index {
__block BOOL responseRecieved = NO;
NSString *stringForURL = [NSString stringWithFormat:#"http://www.thebluealliance.com/api/v1/event/details?event=%#",[[set allObjects] objectAtIndex:index]];
NSURL *url = [NSURL URLWithString:stringForURL];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSLog(#"URL IS GO: %#", stringForURL);
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:queue completionHandler:^(NSURLResponse *_response, NSData *_data, NSError *_error) {
NSLog(#"CHECKED DATA RETURNED AT INDEX %i", index);
NSError *error;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableContainers error:&error];
if (!_regionalDetails) {
_regionalDetails = [[NSMutableArray alloc] init];
}
[_regionalDetails addObject:dict];
responseRecieved = YES;
}];
regionalSchedulesToGet = [set count];
while (responseRecieved == NO) {}
[[MBProgressHUD HUDForView:[[UIApplication sharedApplication] keyWindow]] setLabelText:[NSString stringWithFormat: #"Getting regional %i of %i", index+2, [set count]]];
if (index+1 < [set count]) {
[self getRegionalInformationFromChecked:set atIndex:index+1];
} else {
[[MBProgressHUD HUDForView:[[UIApplication sharedApplication] keyWindow]] setLabelText:#"Writing to file"];
}
}
When the asynchronous request's block is commented out, the MBProgressHUD displays its value fine. But when the block is inserted, the SDK refuses to update the progress indicator, even after leaving the block (after which any threading issues should have been resolved). It does not update until there are no more requests to display, at which point it reads "Writing to file".
Why does an asynchronous request seem to block the main thread, and why can I not make changes on the main thread immediately before or after the request is called?
With
while (responseRecieved == NO) {}
you block the main thread (probably with almost 100% CPU load) until the asynchronous block has finished. Then you call your
function recursively, start another asynchronous block and block again until that has
finished. Therefore the program control does not return to the main runloop until all
operations have finished. Only then the UI updates are done.
Instead of waiting synchronously (which is always a bad idea),
you should start the next operation at the end of the completion block.
Note also that the queue argument of sendAsynchronousRequest is the queue on which
the completion handler is called, so you can just use [NSOperationQueue mainQueue].
Then your code looks roughly like this:
- (void)getRegionalInformationFromChecked:(NSSet *)set atIndex:(NSInteger)index
{
[[MBProgressHUD HUDForView:[[UIApplication sharedApplication] keyWindow]]
setLabelText:[NSString stringWithFormat:#"Getting regional %i of %i", index+1, [set count]]];
NSString *stringForURL = [NSString stringWithFormat:#"http://www.thebluealliance.com/api/v1/event/details?event=%#",[[set allObjects] objectAtIndex:index]];
NSURL *url = [NSURL URLWithString:stringForURL];
NSLog(#"URL IS GO: %#", stringForURL);
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *_response, NSData *_data, NSError *_error) {
NSLog(#"CHECKED DATA RETURNED AT INDEX %i", index);
NSError *error;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableContainers error:&error];
if (!_regionalDetails) {
_regionalDetails = [[NSMutableArray alloc] init];
}
[_regionalDetails addObject:dict];
if (index+1 < [set count]) {
[self getRegionalInformationFromChecked:set atIndex:index+1];
} else {
[[MBProgressHUD HUDForView:[[UIApplication sharedApplication] keyWindow]] setLabelText:#"Writing to file"];
// ... perhaps call a completion function from here ?
}
}];
}
But note that the initial call to getRegionalInformationFromChecked will now
return almost immediately (that's how asynchronous tasks work :-).
Try to dispatch on the main thread all the methods that involve UI refresh

AFnetworking waitUntilFinish not working

I am making a project that uses the AFNetworking Library. I use it to check for a login and password with the webservice. This gives me a 200-code back if it is ok. If it is 200, I set a boolean to true so that the application can continue. But this boolean is not set at the right time. I always need to press two times on my login button before it works. Here is my code to set the boolean.
- (BOOL)credentialsValidated {
self.progressHUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
self.progressHUD.labelText = #"Loading";
self.progressHUD.mode = MBProgressHUDModeIndeterminate;
self.progressHUD.dimBackground = YES;
[[API sharedInstance] loginCommand:[NSMutableDictionary dictionaryWithObjectsAndKeys:_txtLogin.text,#"email",_txtPass.text,#"pwd", nil] onCompletion:^(NSDictionary *json){
//completion
if(![json objectForKey:#"error"]){
NSLog(#"status %#",[json valueForKeyPath:#"data.status"]);
if([[json valueForKeyPath:#"data.status"]intValue] == 200){
//Create user object
_loginstatus = YES;
[self.progressHUD hide:YES afterDelay:5];
}else{
//show validation
_txtLogin.text = #"";
_txtPass.text = #"";
_loginstatus = NO;
}
}else {
NSLog(#"Cannot connect to the server");
}
}];
if(_loginstatus){
NSLog(#"true");
}else{
NSLog(#"false");
}
[self.progressHUD hide:YES afterDelay:5];
return _loginstatus;
}
And here is my API code.
-(void)loginCommand:(NSMutableDictionary *)params onCompletion:(JSONResponseBlock)completionBlock{
NSLog(#"%#%#",kAPIHost,kAPILogin);
NSMutableURLRequest *apiRequest = [self multipartFormRequestWithMethod:#"POST" path:kAPILogin parameters:params constructingBodyWithBlock:^(id <AFMultipartFormData>formData){
//TODO: attach file if needed
}];
AFJSONRequestOperation *operation = [[AFJSONRequestOperation alloc] initWithRequest:apiRequest];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject){
//success !
NSLog(#"SUCCESSSS!");
completionBlock(responseObject);
}failure:^(AFHTTPRequestOperation *operation, NSError *error){
//Failure
NSLog(#"FAILUREE!");
completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription] forKey:#"error"]);
}];
[operation start];
}
When I put below [operation start] the [operation waitUntilFinish] code, the app crashes.
Can somebody help me with this?
Kind regards
LOG
Here is a print of my log after I 2 two times pressed the login button
2012-12-28 09:36:00.547 Offitel[6532:907] http://virtuele-receptie.******.sanmax.be/nl/webservice/company-user/****/*****/**************
2012-12-28 09:36:00.561 Offitel[6532:907] false
2012-12-28 09:36:01.604 Offitel[6532:907] SUCCESSSS!
2012-12-28 09:36:01.605 Offitel[6532:907] status 200
2012-12-28 09:36:20.742 Offitel[6532:907] aanmelden pressed
2012-12-28 09:36:20.746 Offitel[6532:907] http://virtuele-receptie.******.sanmax.be/nl/webservice/company-user/****/*****/**************
2012-12-28 09:36:20.748 Offitel[6532:907] true
2012-12-28 09:36:22.184 Offitel[6532:907] SUCCESSSS!
2012-12-28 09:36:22.184 Offitel[6532:907] status 200
If you do this
return _loginstatus;
You are not waiting the operation to finish, so you're not returning what your server is sending to your application. What I usually do in this cases is display an UIActivityIndicator when the login action is fired, and do nothing till' the server has responded. Then, I set the proper values to any variables and in this case I would segue to home screen.
Remember that when you're using AFNetworking you are always doing async stuff.
EDIT
Code example:
- (void)loginActionWithPassword:(NSString *)pass
{
// Add progress HUD to show feedback
// I'm using MBProgressHUD library: https://github.com/jdg/MBProgressHUD#readme
self.progressHUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
self.progressHUD.labelText = #"Loading";
self.progressHUD.mode = MBProgressHUDModeIndeterminate;
self.progressHUD.dimBackground = YES;
// Launch the action to the server
[Actions loginWithEmail:[self.textEmail.text lowercaseString] andPassword:pass success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
if ([Actions requestFailed:JSON]) {
LOG(#"ERROR %# / %# / %# \n", pass, request, response, JSON);
if ([JSON[#"MSG"] isEqualToString:#"USER_NOT_REGISTERED"]) {
self.progressHUD.labelText = #"User doesn't exist";
self.textEmail.text = #"";
self.textPassword.text = #"";
} else if ([JSON[#"MSG"] isEqualToString:#"PASSWORD_INCORRECT"]) {
self.progressHUD.labelText = #"Wrong password";
self.textPassword.text = #"";
}
[self.progressHUD hide:YES afterDelay:[Utils hudDuration]];
return;
}
// If everything went OK, go to this function to save user data
// and perform segue to home screen (or dismiss modal login view)
[self onLoginSuccessWithPassword:pass JSON:JSON response:response];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
// If something fails, display an error message
LOG(#"ERROR:(%#): %# / %# / %# / %# \n", pass, request, response, error, JSON);
self.progressHUD.labelText = #"Something went wrong, try again";
[self.progressHUD hide:YES afterDelay:[Utils hudDuration]];
return;
}];
}

Xcode, ensure codes after blocks run later for NSURLConnection asynchronous request

Hi there: I have been writing an iOS program which uses many http queries to the backend rails server, and hence there are tons of codes like below. In this case, it is updating a UITableView:
//making requests before this...
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse* response, NSData* data, NSError* error)
{
NSLog(#"Request sent!");
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSLog(#"Response code: %d", [httpResponse statusCode]);
if ([data length] > 0 && error == nil){
NSLog(#"%lu bytes of data was returned.", (unsigned long)[data length]); }
else if ([data length] == 0 &&
error == nil){
NSLog(#"No data was returned.");
}
else if (error != nil){
NSLog(#"Error happened = %#", error); }
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
if (jsonObject != nil && error == nil){
NSLog(#"Successfully deserialized...");
if ([jsonObject isKindOfClass:[NSDictionary class]]){
NSDictionary *deserializedDictionary = (NSDictionary *)jsonObject;
NSLog(#"Dersialized JSON Dictionary = %#", deserializedDictionary);
[listOfItems addObject:deserializedDictionary];
}
else if ([jsonObject isKindOfClass:[NSArray class]]){
NSArray *deserializedArray = (NSArray *)jsonObject;
NSLog(#"Dersialized JSON Array = %#", deserializedArray);
[listOfItems addObjectsFromArray:deserializedArray];
}
else {
/* Some other object was returned. We don't know how to deal
with this situation as the deserializer only returns dictionaries
or arrays */ }
}
else if (error != nil){
NSLog(#"An error happened while deserializing the JSON data., Domain: %#, Code: %d", [error domain], [error code]);
}
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
}];
//the place where never runs
NSLog(#"End of function.");
Here is the problem: the last line gets executed usually before the code block. How do I ensure that the code after block actually runs after the block?
I am aware that the block uses some other threads, which is why I use performSelectorOnMainThread function instead of a direct call of [self.tableView reloadData]. But if I want to do something else afterward, how am I supposed to do?
Also, can anyone show some better ways to do this? I am trying to figure out the best way to make massive calls to the backend. There are several ways to make asynchronous requests, including this block way and another old-fashioned way invoking delegate classes. In the progress to refactor the codes, I also tried to create my own delegate class and let other classes invoke that, but it is difficult to identify the correct behaviour of callback functions for which connection's data it returns, especially for classes that use multiple functions to call different requests. And I don't want to use synchronous calls.
Thanks very much for any answers. Also welcome to point out any bugs in the code.
You can using dispatch group
Sample code:
- (void)doSomethingAndWait {
// synchronous method
// called in main thread is not good idea.
NSAssert(! [NSThread isMainThread], #"this method can't run in main thread.");
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
//making requests before this...
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse* response, NSData* data, NSError* error)
{
// your work here.
dispatch_group_leave(group);
}];
// wait for block finished
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
//will call until block is finished.
NSLog(#"End of function.");
}
And to call that method, you need avoid call it in main thread.
you should call it like this
dispatch_queue_t queue = dispatch_queue_create("com.COMPANYNAME.APPNAME.TASKNAME", NULL);
dispatch_async(queue, ^{
[self doSomethingAndWait];
});

Converting from ASIHTTPRequest to AFNetworking

I am converting my app routines from ASIHTTP to AFNetworking due to the unfortunate discontinuation of work on that project ... and what I found out later to be the much better and smaller codebase of AFNetworking.
I am finding several issues. My code for ASIHTTPRequest is built as a method. This method takes a few parameters and posts the parameters to a url ... returning the resulting data. This data is always text, but in the interests of making a generic method, may sometimes be json, sometimes XML or sometimes HTML. Thus I built this method as a standalone generic URL downloader.
My issue is that when the routine is called I have to wait for a response. I know all the "synchronous is bad" arguments out there...and I don't do it a lot... but for some methods I want synchronous.
So, here is my question. My simplified ASIHTTP code is below, followed by the only way i could think of coding this in AFNetworking. The issue I have is that the AFNetworking sometimes does not for the response before returning from the method. The hint that #mattt gave of [operation waitUntilFinished] totally fails to hold the thread until the completion block is called... and my other method of [queue waitUntilAllOperationsAreFinished] does not necessarily always work either (and does NOT result in triggering the error portion of the [operation hasAcceptableStatusCode] clause). So, if anyone can help, WITHOUT The ever-present 'design it asynchronously', please do.
ASIHTTP version:
- (NSString *) queryChatSystem:(NSMutableDictionary *) theDict
{
NSString *response = [NSString stringWithString:#""];
NSString *theUrlString = [NSString stringWithFormat:#"%#%#",kDataDomain,kPathToChatScript];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:[NSURL URLWithString:theUrlString]];
for (id key in theDict)
{
[request setPostValue:[theDict objectForKey:key] forKey:key];
}
[request setNumberOfTimesToRetryOnTimeout:3];
[request setAllowCompressedResponse:YES];
[request startSynchronous];
NSError *error = [request error];
if (! error)
{
response = [request responseString];
}
return response;
}
AFNetworking version
- (NSString *) af_queryChatSystem:(NSMutableDictionary *) theDict
{
NSMutableDictionary *theParams = [NSMutableDictionary dictionaryWithCapacity:1];
for (id key in theDict)
{
[theParams setObject:[theDict objectForKey:key] forKey:key];
}
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:kDataDomain]];
NSMutableURLRequest *theRequest = [httpClient requestWithMethod:#"POST" path:[NSString stringWithFormat:#"/%#",kPathToChatScript] parameters:theParams];
__block NSString *responseString = [NSString stringWithString:#""];
AFHTTPRequestOperation *operation = [[[AFHTTPRequestOperation alloc] initWithRequest:theRequest] autorelease];
operation.completionBlock = ^ {
if ([operation hasAcceptableStatusCode]) {
responseString = [operation responseString];
NSLog(#"hasAcceptableStatusCode: %#",responseString);
}
else
{
NSLog(#"[Error]: (%# %#) %#", [operation.request HTTPMethod], [[operation.request URL] relativePath], operation.error);
}
};
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[queue addOperation:operation];
[queue waitUntilAllOperationsAreFinished];
[httpClient release];
return responseString;
}
Thanks very much for any ideas.
- (void)af_queryChatSystem:(NSMutableDictionary *) theDict block:(void (^)(NSString *string))block {
...
}
Now within the completionBlock do:
block(operation.responseString);
block will act as the delegate for the operation. remove
-waitUntilAllOperationsAreFinished
and
return responseString
You call this like:
[YourInstance af_queryChatSystem:Dict block:^(NSString *string) {
// use string here
}];
Hope it helps. You can refer to the iOS example AFNetworking has
I strongly recommend to use this opportunity to convert to Apple's own NSURLConnection, rather than adopt yet another third party API. In this way you can be sure it won't be discontinued. I have found that the additional work required to get it to work is minimal - but it turns out to be much more robust and less error prone.
My solution is manually to run the current thread runloop until the callback have been processed.
Here is my code.
- (void)testRequest
{
MyHTTPClient* api = [MyHTTPClient sharedInstance]; // subclass of AFHTTPClient
NSDictionary* parameters = [NSDictionary dictionary]; // add query parameters to this dict.
__block int status = 0;
AFJSONRequestOperation* request = [api getPath:#"path/to/test"
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// success code
status = 1;
NSLog(#"succeeded");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// failure
status = 2;
NSLog(#"failed");
}];
[api enqueueHTTPRequestOperation:request];
[api.operationQueue waitUntilAllOperationsAreFinished];
while (status == 0)
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate date]];
}
STAssertEquals(status, 1, #"success block was executed");
}