I know that people have asked this but I have not found satisfactory answers. I have one method that I send all my URLRequests through. I return the response of the request as a string when the method completes. I have recently added ssl to my program. This means that I can no longer use a synchronous request because I need to take advantage of the didReceiveAuthenticationChallenge function as my credentials are currently self-signing. The program needs the response from the URL in order to continue so there is not harm in waiting for the response. However, I cannot seem to find a way to just hold the code up and continue once completed. I can alert the original function that called to request function but I would like the program to pick up right after that call. And it has unique code below such calls so I cannot specialize the connectionDidFinishLoading: function because each method who calls this is different.
How can I pause the program so I can return the nsdata from the connection to the methods that called it?
Here is some pseudo-code to show you what I mean:
- (void) login:(NSString *)username :(NSString *)password {
NSString *str = [NSString stringWithFormat:%#"%#:::%#",username,password];
NSURL *url = [NSURL urlWithString:#"https://blahblahblah"];
NSString *result = [self connectToUrl:str:url];
if ([result isEqualToString:#"valid"]) {
//this would be more complex in here
NSLog(#"hooray");
} else {
NSLog(#"bummer");
}
}
- (NSString *)connectToUrl:(NSURL *)url :(NSString *)str {
NSData *FileData = [str dataUsingEncoding: NSUTF8StringEncoding];
NSMutableData *data = [[NSMutableData alloc] initWithCapacity:100];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"POST"];
//set up the rest of the request...
...
connection = [NSURLConnection connectionWithRequest:request delegate:self];
[connection start];
//WOULD LIKE TO PAUSE HERE UNTIL COMPLETE! THEN CONTINUE
// received data is assigned in didReceiveData: method
return [[[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding] autorelease];
}
But alas, I cannot do this because I cannot make the final line wait until the connection is complete... Please help me!
Very appreciative!
R
iOS and OS X and much of the Cocoa/Cocoa touch frameworks are built on an event model. You don't pause your app. That's not the proper approach. You need to start the connection and then move on. When the connection completes, you act on that event.
In other words, your login method can't sit and wait for the result. It should start the connection and return.
When you get the result of the connection you call some method to process the login result.
Making use of blocks can make things like this easier but there are other ways. You just need to stop thinking about such things in a linear fashion. Dealing with asynchronous processing requires a different approach.
Related
Ive read quite bit about blocks by now, Apple's Guide, Cocoabuilder, 3 articles on SO and ive used examples in my code that I basically got from online tutorials. Im still trying to understand one specific question. So I decided to make an app with nothing more than a completionHandler example to understand better. This is what I came up with:
ViewController
- (void)viewDidLoad
[SantiappsHelper fetchUsersWithCompletionHandler:^(NSArray *users) {
self.usersArray = [NSMutableArray array];
for (NSDictionary *userDict in users) {
[self.usersArray addObject:[userDict objectForKey:#"username"]];
}
//WHILE TESTING postarray method, comment this out...
//[self getPoints];
[self.tableView reloadData];
}];
}
SantiappsHelper.h/m
typedef void (^Handler)(NSArray *users);
+(void)fetchUsersWithCompletionHandler:(Handler)handler {
NSString *urlString = [NSString stringWithFormat:#"http://www.myserver.com/myapp/getusers.php"];
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10];
[request setHTTPMethod: #"GET"];
__block NSArray *usersArray = [[NSArray alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//dispatch_async(dispatch_get_main_queue(), ^{
// Peform the request
NSURLResponse *response;
NSError *error = nil;
NSData *receivedData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error) {
// Deal with your error
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
return;
}
NSLog(#"Error %#", error);
return;
}
NSString *responseString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
usersArray = [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSASCIIStringEncoding] options:0 error:nil];
if (handler){
//dispatch_sync WAITS for the block to complete before returning the value
//otherwise, the array is returned but gets zeroed out
dispatch_sync(dispatch_get_main_queue(), ^{
handler(usersArray);
});
}
});
}
Here is what I understand...
I call fetchUsersWithCompletionHandler from my VC & pass it this completion block. That block takes an NSArray users parameter & returns void.
Meanwhile in the SantiappsHelper Class we have a variable called handler of type ^block which it receives from VC.
The fetchUsersWithCompletionHandler method runs, taking that CompletionBlock parameter, which itself takes the NSArray users parameter? a little confusing.
The webfetch is dispatch_async so it wont block the main thread. So execution on the main thread continues. That new thread executes the fetch synchronously that new thread will stall until the response is returned. Once that new thread receives the response, it fills in the NSHTTPURLResponse. Once it returns, it fills in usersArray with the NSJSONSerialized data.
Finally it reaches the if test and it checks for the existence of the PARAMETER handler that was passed in? A little confusing...the parameter passed in was the completionBlock. Wouldnt that handler parameter always and obviously exist since it was passed in?
Once the handler !NULL then execution is returned to the main thread passing back the NSArray users expected by the block back in the VC.
But if I change the second dispatch to async, the usersArray is properly populated but once handler(usersArray) is sent back to the main thread, its empty or nil! Why?
Correct. The best way to say/think about this is to say that you are invoking a method called fetchUsersWithCompletionHandler:. This method will go away and do some work and at some point in the future it may execute the code you have declared in the block literal and pass in an array of users.
The method takes an argument called handler of type void (^)(NSArray *users). This type represents a block of code that when invoked should receive and array and return no result.
The fetchUsersWithCompletionHandler: does some work and at some point may invoke the block passed in with an array of users as the blocks argument.
Correct
The if (handler) { checks to see if the handler arguments is not nil. In most cases this would be the case especially if you always invoke the fetchUsersWithCompletionHandler: with a block literal, but you could always invoke the method with [self fetchUsersWithCompletionHandler:nil]; or invoked it passing along a variable from somewhere else as the completion, which may be nil. If you try to dereference nil to invoke it then you will crash.
Execution is not "passed back" to the main thread, you are simply enqueueing a block of work to be performed on the main thread. You are doing this with dispatch_sync call which will block this background thread until the block completes - this isn't really required.
The array being nil could be a consequence of you declaring the usersArray with __block storage. This is not required as you are not modifying what usersArray is pointing to at any point you are simply calling methods on it.
I have send asynchronous request to a website using the following code:
NSMutableURLRequest *requestSiteToSendData = [NSMutableURLRequest requestWithURL:[[NSURL alloc]initWithString:#"www.example.com"] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30];
NSURLConnection *connectionToSiteToSendData = [[NSURLConnection alloc]initWithRequest:requestSiteToSendData delegate:self];
Then I used the following method defined inside NSURLConnectionDelegate to get and parse the data after the data fetching is completed.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
//parse 'data'
NSString *parsedData = [self parseDataWithData:data];
}
And then in the method in which I send the asynchronous request, I return parsedData. But the returning should only happen after the data fetching is completed and hence parsing is done. I know the question arises if that is what I need then why I am not using synchronous request. It is because I don't want my other methods to hang up when the loading is going on in background.
Quick answer : if it's asynchronous, you don't want to wait the asynchronous method.
One of the bests option would be :
The object calling wanting the data should set itself as the object that runs the asynchronous method, and in didReceiveData, you call a method such as updateData:(NSString *)parsedData, which handles the newly received data
The object calling the method should use KVO to observe any change on a property of the object that runs the asynchronous method.
Tell me if you need more informations.
Asynchronous requests run on separate thread, So we don't need to worry about handling view lockup.
If you want send a synchronous request then you have to use GCD to achieve the same. And various other details like, how much data is send/received etc. will not be available in synchronous request.
Synchronous request are helpful if your code next state is dependent on data received in response of the request.
As far as i understand you want that to return data after web call is complete. so i would suggest that create any method for webcall that returns NSData and do something like this:
NSHTTPURLResponse* urlResponse = nil;
NSError *error = [[NSError alloc] init];
NSData *responseData = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:&urlResponse error:&error];
if ([urlResponse statusCode] >= 200 && [urlResponse statusCode] < 300) {
// return responseData from here.
}
else {
NSLog(#"%d",[urlResponse statusCode]);
NSString *result = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
NSLog(#"%#",result);
}
and you don't want to hung up your View. so call this method in background thread. like this:
[self performSelectorInBackground:#selector(WebCallMethod) withObject:nil];
Hope it Helps!!
You have this delegate method which will execute when all the downloading is completed from tha server successfully.Use this method to do the remaining process
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// do something with the data
// receivedData is declared as a method instance elsewhere
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
}
Ok this method gets executed when the all data is recieved.What you should do is to collect all data from -didReceiveData delegate method and then use it to parse in this method.
A must read document for you
In my app, the root view controller acquires information from the internet, parses it, and displays it in viewDidAppear. I am using this method because my app in embedded in a UINavigationController and this way the root view controller will reload its data when the user presses the back button and pops to the root view.
When this occurs, it takes some time for the information from the internet to be acquired and displayed. During this time, if the user clicks a button to move to a different view, the button action will not occur until the view controller has completed its process of acquiring the web data.
How can I make it so that the buttons will override the other processes and immediately switch the view? Is this safe? Thanks in advance.
Edit
Here's an example of a portion where I take the information off of the site (My app parses the HTML).
NSURL *siteURL = [NSURL URLWithString:#"http://www.ridgefield.org/ajax/dist/emergency-announcements"];
NSError *error;
NSString *source = [NSString stringWithContentsOfURL:siteURL
encoding:NSUTF8StringEncoding
error:&error];
This is where Apple folks would hound, "don't block the main thread!".
The primary suggestion for this kind of workflow is to use a separate thread (read: queue) for loading data from the web. Then the worker who completes the load can set some property on your view controller, and inside that setter is where the UI should be updated. Remember to call the setter back on the main thread.
There are several ways to skin the concurrency cat, but the answer to this particular question leaves them out of scope. The short answer is to not do the load in the main thread, and that should lead you to the right place.
The NSString method stringWithContentsOfURL is synchronous and will block your main thread.
Instead of using background threads to solve the problem, you can use asynchronous URL requests. This does not block the user interface because a delegate protocol is used to let you know when the request is complete. For example:
NSURL *url = [NSURL URLWithString:#""http://www.ridgefield.org/ajax/dist/emergency-announcements""];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLConnection* theConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
And then some of your delegate methods are:
-(void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)theResponse
{
// create received data array
_receivedData = [[NSMutableData alloc] init];
}
-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)theData
{
// append to received data.
[_receivedData appendData:theData];
}
-(void)connectionDidFinishLoading:(NSURLConnection*)connection
{
// now the connection is complete
NSString* strResult = [[NSString alloc] initWithData: _receivedData encoding:NSUTF8StringEncoding];
// now parse strResult
}
According to Best way to remove from NSMutableArray while iterating?, we can't remove an object from NSMutableArray while iterating, yes.
But, what if I have a code like the following
- (void)sendFeedback {
NSMutableArray *sentFeedback = [NSMutableArray array];
for (NSMutableDictionary *feedback in self.feedbackQueue){
NSURL *url = [NSURL URLWithString:#"someApiUrl"];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:[feedback objectForKey:#"data"] forKey:#"data"];
[request setCompletionBlock:^{
[sentFeedback addObject:feedback];
}];
[request startAsynchronous];
}
[self.feedbackQueue removeObjectsInArray:sentFeedback];
}
I'm using a NSRunLoop to create a NSThread to execute the sendFeedback method every a period of time. The way I sent data to the API is by using Asynchronous method (which will create a background thread for each request) Once the feedback has been sent, it has to be removed before NSRunner execute this method at the next period to avoid duplicate data submission.
By using asynchronous, the loop (the main thread) will continue running without waiting for the response from server. In some cases (maybe most cases), the loop will finish running before all the response from server of each request come back. If that so, the completion block's code will be execute after the removeObjectsInArray which will result in sent data remains in self.feedbackQueue
I'm pretty sure that there are several ways to avoid that problem. But the only one that I can think of is using Synchronous method instead so that the removeObjectsInArray will not be execute before all the request's response are come back (Either success or fail). But if I do so, it's mean that the internet connection has to be available for longer period. The time needed to the sendFeedback's thread will be longer. Even it will be run by newly created NSThread which will not cause the app to not respond, resources will be needed anyways.
So, is there any other way besides the one I mentioned above? Any suggestion are welcome.
Thank you.
There are a few ways to deal with this kind of problem. I suggest using a dispatch group to synchronize your feedback and using an instance variable to keep from executing a new feedback batch while one is still in progress. For this example, let's assume you create an instance variable named _feedbackUploadInProgress to your class, you could rewrite your -sendFeedback method like this:
- (void)sendFeedback
{
if( _feedbackUploadInProgress ) return;
_feedbackUploadInProgress = YES;
dispatch_group_t group = dispatch_group_create();
NSMutableArray *sentFeedback = [NSMutableArray array];
for (NSMutableDictionary *feedback in self.feedbackQueue) {
// enter the group for each item we're uploading
dispatch_group_enter(group);
NSURL *url = [NSURL URLWithString:#"someApiUrl"];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:[feedback objectForKey:#"data"] forKey:#"data"];
[request setCompletionBlock:^{
[sentFeedback addObject:feedback];
// signal the group each time we complete one of the feedback items
dispatch_group_leave(group);
}];
[request startAsynchronous];
}
// this next block will execute on the specified queue as soon as all the
// requests complete
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self.feedbackQueue removeObjectsInArray:sentFeedback];
_feedbackUploadInProgress = NO;
dispatch_release(group);
});
}
One approach is to keep track of the requests inflight and do the queue clean up when they are all done. Keeping track with the block is a little tricky because the naive approach will generate a retain cycle. Here's what to do:
- (void)sendFeedback {
NSMutableArray *sentFeedback = [NSMutableArray array];
// to keep track of requests
NSMutableArray *inflightRequests = [NSMutableArray array];
for (NSMutableDictionary *feedback in self.feedbackQueue){
NSURL *url = [NSURL URLWithString:#"someApiUrl"];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
// save it
[inflightRequests addObject:request];
// this is the ugly part. but this way, you can safely refer
// to the request in it's block without generating a retain cycle
__unsafe_unretained ASIFormDataRequest *requestCopy = request;
[request setPostValue:[feedback objectForKey:#"data"] forKey:#"data"];
[request setCompletionBlock:^{
[sentFeedback addObject:feedback];
// this one is done, remove it
// notice, since we refer to the request array here in the block,
// it gets retained by the block, so don't worry about it getting released
[inflightRequests removeObject:requestCopy];
// are they all done? if so, cleanup
if (inflightRequests.count == 0) {
[self.feedbackQueue removeObjectsInArray:sentFeedback];
}
}];
[request startAsynchronous];
}
// no cleanup here. you're right that it will run too soon here
}
I am writing a network class for an iOS app. This class will take care of all logging and network traffic. I have a problem where I have to send possibly thousands of requests at one time, but NSURLConnections are timing out because the delegate methods will not be called until all the NSURLConnections are started, by which time the timeout period has expired. I am using a rest API for Drupal and, unfortunately, I do not know of a way to create multiple instances with one request. How can I receive responses while simultaneously sending them? If I use GCD to pass off the creation of the NSURLConnections, will that solve the problem? I think I would have to pass the entire operation of iterating over the objects to send and sending to GCD to free up the main thread to answer to responses.
-(BOOL)sendOperation:(NetworkOperation)op
NetworkDataType:(NetworkDataType)dataType
JsonToSend:(NSArray *)json
BackupData:(NSArray *)data
{
if(loggingMode)
{
return YES;
}
NSURLConnection *networkConnection;
NSData *send;
NSString *uuid = [self generateUUID];
NSMutableArray *connections = [[NSMutableArray alloc] init];
NSMutableURLRequest *networkRequest;
for (int i=0; i<[json count] && (data ? i<[data count] : YES); i++)
{
if(op == Login)
{
/*Grab all cookies from the server domain and delete them, this prevents login failure
because user was already logged in. Probably find a better solution like recovering
from the error*/
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:
[[NSURL alloc] initWithString:networkServerAddress]];
for (NSHTTPCookie *cookie in cookies)
{
[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
}
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/user/login"]]];
}
else if(op == StartExperiment)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/node"]]];
}
else if(op == Event || op == EndExperiment || op == SendAll)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/node"]]];
}
else if(op == Logout)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/user/logout"]]];
}
send = [[json objectAtIndex:i] dataUsingEncoding:NSUTF8StringEncoding];
//Set the headers appropriately
[networkRequest setHTTPMethod:#"POST"];
[networkRequest setValue:#"application/json"
forHTTPHeaderField: #"Content-type"];
[networkRequest setValue:[NSString stringWithFormat:#"%d", [send length]]
forHTTPHeaderField:#"Content-length"];
[networkRequest setValue:#"application/json"
forHTTPHeaderField:#"Accept"];
//Set the body to the json encoded string
[networkRequest setHTTPBody:send];
//Starts async request
networkConnection = [[NSURLConnection alloc] initWithRequest:networkRequest delegate:self];
//Successfully created, we are off
if(networkConnection)
{
[networkConnectionsAndData setValue:[[NSMutableArray alloc] initWithObjects:uuid,
[[NSNumber alloc] initWithInt:op], [[NSNumber alloc] initWithInt:dataType], [[NSMutableData alloc] init], (data ? [data objectAtIndex:i] : [NSNull null]), nil]
forKey:[networkConnection description]];
}
else //Failed to conn ect
{
NSLog(#"Failed to create NSURLConnection");
return NO;
}
}
[[self networkOperationAndConnections] setObject:[[NSMutableDictionary alloc] initWithObjectsAndKeys:[[NSMutableArray alloc] initWithObjects:connections, nil], #"connections", [[NSMutableArray alloc] init], #"errors", nil]
forKey:uuid];
return YES;
}
The dictionaries are used to keep track of the correlating data with each NSURLConnection and also to group the NSURLConnections together into one group to determine ultimate success or failure of an entire operation.
Update
AFNetworking was key in finishing this project. It not only cleaned up the code substantially, but dealt with all the threading issues inherit in sending so many requests. Not to mention with AFNetworking I could batch all the requests together into a single operation. Using blocks, like AFNetworking uses, was a much cleaner and better solution than the standard delegates for NSURLConnections.
You definitely need to allow the NSURLRequest / Connection to be operating on another thread. (Not the main thread!)
Edited for clarity**:
I noticed your comment of "//Starts async request" and I wanted to be sure you realized that your call there is not what you would expect out of a typical "asynch" function. Really its just firing off the request synchronously, but since its a web request it inherently behaves asynchronously. You want to actually place these requests on a another thread for full asynch behavior.
Everything else aside, I really suggest digging into Apple's networking example project here: MVCNetworking
As for specifics on your question, there's a couple ways to do this.
One is to keep your connection from starting immediately using initWithRequest:<blah> delegate:<blah> startImmediately:FALSE and then schedule your NSURLConnection instances on another thread's run-loop using: scheduleInRunLoop:forMode:
(Note: You then have to kick off the connection by calling start-- it's best to do this via an NSOperation + NSOperationQueue.)
Or use this static method on NSURLConnection to create/launch the connection instead of doing an alloc/init: sendAsynchronousRequest:queue:completionHandler:
(Note: this approach accomplishes pretty much same as above but obfuscates the details and takes some of the control out of your hands.)
To be honest my quick answers above won't be sufficient to finish this kind of project, and you'll need to do a bit of research to fill in the blanks, especially for the NSOperationQueue, and that's where the MVCNetworking project will help you.
Network connections are a fickle beast -- You can time-out and kill your connections even if they're running on a background thread simply by trying to perform too much work simultaneously! I would seriously reconsider opening up several thousand NSURLConnections at once, and using an NSOperationQueue would help work around this.
ADDITIONAL RESOURCES:
Here's a 3rd party library that may make your networking adventures less painful:
https://github.com/AFNetworking/AFNetworking
http://engineering.gowalla.com/2011/10/24/afnetworking/