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
Related
The following code is freezing my UI. Cant do any actions.
- (void) longPoll {
//create an autorelease pool for the thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError* error = nil;
NSURLResponse* response = nil;
NSURL* requestUrl = [NSURL URLWithString:#"myurl"];
NSURLRequest* request = [NSURLRequest requestWithURL:requestUrl];
//send the request (will block until a response comes back)
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response error:&error];
dispatch_async(dispatch_get_main_queue(), ^{
[self dataReceived:responseData];
});
});
//compose the request
//pass the response on to the handler (can also check for errors here, if you want)
//clear the pool
}
- (void) startPoll {
//not covered in this example: stopping the poll or ensuring that only 1 poll is active at any given time
[self performSelectorInBackground:#selector(longPoll) withObject: nil];
}
- (void) dataReceived: (NSData*) theData {
//process the response here
NSDictionary *dict=[theData JSONValue];
[self ParseJson:dict];
[self performSelectorInBackground:#selector(longPoll) withObject: nil];
}
Can anyone give me the exact reason for it or any alternative to do the similar code for continues polling.
You are creating an infinite loop:
longCall calls dataReceived calls longCall etc....
What exactly you want to do. There is infinite loop between longPool and dataReceived
there should be mechanism where you stop this call and you can use
#autorelease {} block for create autorelease pool in ARC Enabled project and
NSAutoReleasePool class obj for Without ARC.
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];
});
I'm using a UISearchBar in my application and the problem is when I call a few json methods searchBarSearchButtonClicked seems to not resign the keyboard until the other methods are done loading the data. I've tried alternatively using UIAlertView and UIButtons to replace the searchBarSearchButtonClicked function but they appear to literally freeze and stay in a "pressed down" state too. I was also wondering if this would be a reason why [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; wouldn't show an activity indicator in the device's status bar.
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar{
self.args = searchBar.text;
[self grabData];
[self fillVars];
[searchBar resignFirstResponder];
[self.tableView reloadData];
}
[self grabData] is where I grab the JSON data and [self fillVars] just fills a few things that are later used.
-(void)grabData{
self.args = [self.args stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
urlString = [NSString stringWithFormat:#"%#%#?key=%#&q=%#",baseUrl,func,apiKey,args];
url = [NSURL URLWithString:urlString];
NSData *jsonData = [NSData dataWithContentsOfURL:url];
NSError *error;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:&error];
self.matches = [json objectForKey:#"matches"];
[UIApplication sharedApplication].networkActivityIndicatorVisible=YES;
}
You will have to use threading. All manipulation with your interface happens on the main thread, so when you perform a lengthy task on the main thread, the interface won't be able to update itself before the task has completed.
In a UIViewController you can do [self performSelectorInBackground:#selector(grabData) withObject:self], which is a convenience method for dispatching a new queue (thread) using grand central dispact.
You could also do that manually, using the GCD API. You would do something along the lines of this:
dispatch_queue_t jsonQueue = dispatch_queue_create("JSON Queue", NULL);
dispatch_async(jsonQueue, ^{
// fetch JSON data ...
dispatch_async(dispatch_get_main_queue(), ^{
// perhaps do something back on the main queue once you're done!
});
});
I have a program that has multiple url request so I used the the code in http://snippets.aktagon.com/snippets/350-How-to-make-asynchronous-HTTP-requests-with-NSURLConnection and put it in it's own class (class B).To call the class I am simple initializing class B in class A, sending a url to class B's get method([classname get:url]) and then getting the server response upon return.
The problem is that I am getting defeated by race conditions due to the fact that the didReceiveData: method is not complete by the time my method is returned.
I have gone through the developer example of using NSUrlConnection and they are updating views once the response finally came in so they didn't have to fight this problem.
Thank you so much for your help.
I need to keep the calls asynchronous due to the number of them I have to make but I am open to any suggestions.
Edit (moved from answer)
I changed the code to GCD based off of a tutorial and I am still getting defeated by the race condition. Here is the code that I am using now:
I changed it to GCS based on on your suggestion but I am still getting caught by the race condition. Below is the code that I changed it to and I am calling it by:
NSString *responseStringClassA = [InitalizedInstanceOfClassA LogIn:#"username" #"password"];
//Log into the server
-(NSString *)logIn: (NSString *) username password:(NSString *) password
{
NSString* returnString;
dispatch_queue_t downloadQueue = dispatch_queue_create("Login", NULL);
dispatch_async(downloadQueue, ^{
BOOL success = YES;
NSString *urlAsString =[NSString stringWithFormat:#""URL HERE];
NSLog(#"url sent out: %#", urlAsString);
NSURL *url = [NSURL URLWithString:urlAsString];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
NSError *error = nil;
NSData *connectionData = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:nil error:&error];
NSLog(#"Connection Data: %#", [[NSString alloc] initWithData:connectionData encoding:NSASCIIStringEncoding]);
[returnString isEqualToString:[NSString stringWithUTF8String:[connectionData bytes]]];
if ([connectionData length] > 0 && error == nil) {
//success
success = YES;
}
else if([connectionData length] == 0 && error == nil){
//nodata
success = YES;
}
else if(error != nil){
//error ..
success = NO;
}
dispatch_async(dispatch_get_main_queue(), ^{
[returnString isEqualToString:[[NSString alloc] initWithData:connectionData encoding:NSASCIIStringEncoding] ];
});
});
return returnString;
}
It's the very purpose of asynchronous requests that the intial method returns almost immediately without having done the work. Later, when the work has been done, you will be notified and you can access and use the result.
But obviously, you're looking for something else than asynchronous operations. And alternative would be to use synchronous URL requests but run them from separate threads. The best way to achieve this is to use GCD (grand central dispatch).
Note that you may not update the user interface from background threads. Instead, when the URL request has finished and you want to display your results, you have to call performSelectorOnMainThread (part of NSObject) for that.
I'm writing test cases for a wrapper class written around ASIHTTPRequest. For reasons I can't determine, my test cases complete with failure before the ASIHTTPRequest finishes.
Here's how the program flow works.
Start in my test case.
Init my http engine object, instruct it to create a new list
Create the new ASIHTTPRequest object and set it up.
Add the request to an operation queue.
Wait until that queue is empty
Check to see if my delegate methods were called and fail the test if they weren't.
Now, most of the time everything works fine and the test passes, but some of the time it fails because my delegate methods were called AFTER the operation queue returned control to my wait method.
Test Case
// Set my flags to 'NO'
- (void)setUp {
requestDidFinish = NO;
requestDidFail = NO;
}
- (void)testCreateList {
NSString *testList = #"{\"title\": \"This is a list\"}";
JKEngine *engine = [[JKEngine alloc] initWithDelegate:self];
NSString *requestIdentifier = [engine createList:jsonString];
[self waitUntilEngineDone:engine];
NSString *responseString = responseString_;
[engine release];
GHAssertNotNil(requestIdentifier, nil);
GHAssertTrue(requestDidFinish, nil);
GHAssertTrue([responseString hasPrefix:#"{\"CreateOrEditListResult\""], nil);
}
// Puts the test into a holding pattern until the http request is done
- (void)waitUntilEngineDone:(JKEngine *)engine {
[engine waitUntilFinishedRunning];
}
// The delegate method called on successful completion
- (void)requestFinished:(NSString *)requestIdentifier withResponse:(NSString *)response {
NSLog(#"request did finish");
requestDidFinish = YES;
responseIdentifier_ = [requestIdentifier retain];
responseString_ = [response retain];
}
Engine Code
- (NSString *)createList:(NSString *)list {
ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:[NSURL URLWithString:url]];
[request addRequestHeader:#"Content-Type" value:kContentType];
[request setRequestMethod:kPOST];
request.delegate = self;
[request appendPostData:[list dataUsingEncoding:NSUTF8StringEncoding]];
NSString *requestIdentifier = [NSString stringWithNewUUID];
[operationQueue_ addOperation:request];
[operationDictionary_ setObject:request forKey:requestIdentifier];
return requestIdentifier;
}
// This is the ASIHTTPRequest delegate method that's called on success
// but it sometimes isn't called until AFTER the operationQueue finishes running
- (void)requestFinished:(ASIHTTPRequest *)request {
DLog([request responseString]);
BOOL canNotifiyDelegate = [self.delegate respondsToSelector:#selector(requestFinished:withResponse:)];
if (canNotifiyDelegate) {
NSArray *keyArray = [operationDictionary_ allKeysForObject:request];
NSString *requestIdentifier = [keyArray objectAtIndex:0];
[operationDictionary_ removeObjectForKey:requestIdentifier];
if ([keyArray count] != 1) {
ALog(#"It looks like a request was added to the operation dictionary multiple times. There's a bug somewhere.", nil);
}
[self.delegate requestFinished:requestIdentifier withResponse:[request responseString]];
}
}
- (void)waitUntilFinishedRunning {
[operationQueue_ waitUntilAllOperationsAreFinished];
}
This is the way ASIHTTPRequest works. Delegate methods are called on the main thread, and calls to delegates do not block the request thread, so it's perfectly possible your delegates will be called after the queue finishes.
ASIHTTPRequest calls delegate methods on the main thread, by default GH-Unit runs its tests on a background thread. I'm still a little hazy on exactly what was going on, but forcing my network tests to run on the main thread fixed the problem.
I implemented the following method in my network test class.
- (BOOL)shouldRunOnMainThread {
return YES;
}