How can I wait for a certain task to be done?
This is what I'm trying to do:
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:stringURL]];
if (!data) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:#"%#", NOCONNECTIONALERTTITLE] message:[NSString stringWithFormat:#"%#", NOCONNECTIONALERTMESSAGE] delegate:self cancelButtonTitle:[NSString stringWithFormat:#"%#", NOCONNECTIONALERTCANCELBUTTON] otherButtonTitles:nil, nil];
[alert show];
} else { ... }
This if is always false, since its called before data even is initialized. I'm 100% sure, that the connection is established.
I want to use this as an easy alternative to checking the internet-connection programmatically.
dataWithContentsOfURL: is a blocking call, so data is initialised when you try to use it. If it's nil then you likely have an issue with the URL you're trying to load.
Because this is blocking you shouldn't use it on the main thread. And, generally, you shouldn't want to wait for completion - you should embrace the asynchronous nature of network operations and use appropriate asynchronous API and designs to handle them.
Log the NSURL that you are creating to check it exists (and fix if it doesn't).
Your simplest option for running an asynchronous download and handling the result is to use NSURLConnection +sendAsynchronousRequest:queue:completionHandler:. Supply the queue as [NSOperationQueue mainQueue] so that your callback is run on the main thread and you can update your UI.
Related
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.
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/
I have a UITableViewController that when opened displays a table of the following object:
class {
NSString *stringVal;
int value;
}
However, whenever this controller opens, I want it to download the data from the internet and display "Connecting..." in the status bar and refresh the stringVal and value of all of the objects. I do this by refreshing the array in the UITableViewController. However, to do this the UI hangs sometimes or even displays "blank" table cells until the operation has ended. I'm doing this in an NSOperationQueue to download the data, but I'm wondering if there's a better way to refresh the data without those weird UI bugs.
EDIT:
the UI no longer displays blank cells. This was because cellForRowAtIndexPath was setting nil values for my cellText. However, it still seems somewhat laggy when tableView.reloadData is called even though I'm using NSOperationQueue.
EDIT2:
Moreover, I have two problems: 1. the scrolling prevents the UI from being updated and 2. when the scrolling does stop and the UI starts to update, it hangs a little bit. A perfect example of what I'm trying to do can be found in the native Mail app when you view a list of folders with their unread count. If you constantly scroll the tableview, the folders unread count will be updated without any hanging at all.
Based on your response in the question comments, it sounds like you are calling [tableView reloadData] from a background thread.
Do not do this. UIKit methods, unless otherwise specified, always need to be called from the main thread. Failing to do so can cause no end of problems, and you are probably seeing one of them.
EDIT: I misread your comment. It sounds like you are not updating the UI from a background thread. But my comments about the architecture (i.e. why are you updating in a background thread AFTER the download has finished?).
You state that "when the data comes back from the server, I call a background operation..." This sounds backwards. Normally you would have your NSURLConnection (or whatever you are using for the download) run on the background thread so as not to block to UI, then call out to the main thread to update the data model and refresh the UI. Alternatively, use an asynchronous NSURLConnection (which manages its own background thread/queue), e.g.:
[NSURLConnection sendAsynchronousRequest:(NSURLRequest *)
requestqueue:(NSOperationQueue *)queue
completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler];
And just make sure to use [NSOperationQueue mainQueue] for the queue.
You can also use GCD, i.e., nested dispatch_async() calls (the outer to a background queue for handling a synchronous connection, the inner on the main queue to handle the connection response).
Finally, I will note that you in principle can update your data model on the background thread and just refresh the UI from the main thread. But this means that you need to take care to make your model code thread-safe, which you are likely to mess up at least a couple times. Since updating the model is probably not a time consuming step, I would just do it on the main thread too.
EDIT:
I am adding an example of how one might use GCD and synchronous requests to accomplish this. Clearly there are many ways to accomplish non-blocking URL requests, and I do not assert that this is the best one. It does, in my opinion, have the virtue of keeping all the code for processing a request in one place, making it easier to read.
The code has plenty of rough edges. For example, creating a custom dispatch queue is not generally necessary. It blindly assumes UTF-8 encoding of the returned web page. And none of the content (save the HTTP error description) is localized. But it does demonstrate how to run non-blocking requests and detect errors (both at the network and HTTP layers). Hope this is helpful.
NSURL *url = [NSURL URLWithString:#"http://www.google.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
dispatch_queue_t netQueue = dispatch_queue_create("com.mycompany.netqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(netQueue,
^{
// We are on a background thread, so we won't block UI events (or, generally, the main run loop)
NSHTTPURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
dispatch_async(dispatch_get_main_queue(),
^{
// We are now back on the main thread
UIAlertView *alertView = [[UIAlertView alloc] init];
[alertView addButtonWithTitle:#"OK"];
if (data) {
if ([response statusCode] == 200) {
NSMutableString *body = [[NSMutableString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
[alertView setTitle:#"Success"];
[alertView setMessage:body];
}
else {
[alertView setTitle:#"HTTP Error"];
NSString *status = [NSHTTPURLResponse localizedStringForStatusCode:[response statusCode]];
[alertView setMessage:status];
}
}
else {
[alertView setTitle:#"Error"];
[alertView setMessage:#"Unable to load URL"];
}
[alertView show];
[alertView release];
});
});
dispatch_release(netQueue);
EDIT:
Oh, one more big rough edge. The above code assumes that any HTTP status code != 200 is an error. This is not necessarily the case, but handling this is beyond the scope of this question.
I want to put in a timeout in case it takes too long to find my location, send out the relevant url, and parse the xml. It worked when I used performSelector:withObject:afterDelay in the locationManager (just to test getting the xml), but when I put similar code around my parser it doesn't actually abort the parsing. I am testing this by dropping the delay to 0.01.
My problem is: even with the delay set to 0.01, it still waits for all the parsing to complete first, and only then does it put up the alertView that is coded in the parsingDidTimeout method.
I did try this with a timer, and that wasn't working as well as performSelector: does in the other parts of my code. Either way, it doesn't put up the alertView, and stop the parsing, until after the parsing has finished, no matter how long that takes.
I create a url which requires a radius. First I try a small radius, but if I don't get the data I need, I expand the radius and send the url again and parse again. Here is part of my StartParsing method.
xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];
XMLParser *parser = [[XMLParser alloc] initXMLParser];
[xmlParser setDelegate:parser];
if (!hadToExpandRadius){//meaning, only do this the first time I send out the url and parse
[self performSelector:#selector(parsingDidTimeout:) withObject:nil afterDelay:0.01];
}
//Start parsing the XML file.
BOOL success = [xmlParser parse];
if(success){
if((didNotGetTheDataYet) && (radius < 500)){
hadToExpandRadius = YES;
radius = radius + 35;
[self startParsing];//do this same method, with larger radius
}
else {
NSLog(#"No Errors");
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(parsingDidTimeout:) object:nil];}
[parser release];
}
-(void)parsingDidTimeout{
[xmlParser abortParsing];
UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:#"Try Later" message:#"We need a better connection. We can get the data later." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[servicesDisabledAlert show];
[servicesDisabledAlert release];
[myActivityView stopAnimating];
}
Thank you for your help.
Calling performSelector:withObject:afterDelay: you ask the run loop to call the selector later. But [xmlParser parse] blocks the run loop, so it doesn't have a chance to call you selector.
abortParsing is designed to be called inside parsers' delegate methods.
The workaround can be to parse in a separate thread.
Found it -- just extra ":" in my performSelector:#selector(parsingDidTimeout:)!
I thought it was something fancy having to do with the second thread. Just syntax.
Thanks for explaining about the parse blocking the run loop. I was hoping not to need another thread, but your suggestion fixed my problem. Thanks.
See also:
Objective-C Asynchronous Web Request with Cookies
I spent a day writing this code and can anyone tell me what is wrong here?
WSHelper is inherited from NSObject, I even tried NSDocument and NSObjectController and everything..
-(void) loadUrl: (NSString*) urlStr{
url = [[NSURL alloc] initWithString:urlStr];
request = [NSURLRequest requestWithURL:url cachePolicy: NSURLRequestReloadIgnoringCacheData timeoutInterval: 60.0];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
if(connection)
{
receivedData = [[NSMutableData data] retain];
//[connection start];
}
else
{ display error etc... }
NSApplication * app = [NSApplication sharedApplication];
[app runModalForWindow: waitWindow];// <-- this is the problem...
}
-(void)connection: (NSURLConnection*)connection didReceiveData:(NSData*)data{
progressText = #"Receiving Data...";
[receivedData appendData:data];
}
-(void)connection: (NSURLConnection *)connection didFailWithError:(NSError *)error{
progressText = #"Error...";
NSAlert * alert = [[NSAlert alloc] init];
[alert setMessageText:[error localizedDescription]];
[alert runModal];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
progressText = #"Done...";
pData = [[NSData alloc] initWithData:receivedData];
[self hideWindow];
}
The code just wont do anything, it doesnt progress at all. I even tried it with/without startImmediately:YES but no luck !!!, this is executed in main window so even the thread and its run loop is running successfully.
I tried calling synchronous request, and it is working correctly !! But I need async solution.
I have added CoreServices.Framework in project, is there anything more I should be adding to the project? any compiler settings? Or do i have to initialize anything before I can use NSURLConnection?
Any solution to run NSURLConnection on different thread on its own NSRunLoop, Objective-C and MAC Development has no sample code anywhere in documentation that makes everything so difficult to code.
I also met the same problem that didn't get the delegate method called when using NSURLConnection in a Modal Window.
after some investigation, following code resolve it.
NSURLConnection* conn = [[NSURLConnection alloc] initWithRequest:requst delegate:self startImmediately:NO];
[conn scheduleRunLoop:[NSRunLoop currentRunLoop] forMode:NSModalPanelRunLoopMode];
[conn start];
However, when connectionDidFinishLoading called, [NSApp stopModal] doesn't work, need call [NSApp abortModal] instead.
Firstly you're making starting the connection too complicated. Change to:
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]
Remove [connection start]. Now:
Is your app definitely running the run loop normally? NSURLConnection requires this to work.
Are you able to perform a synchronous load of the URL request?
In the debugger, can you see that url is what you expect it to be? What is it?
Is it possible that you're deallocating WSHelper before any delegate messages are received? NSURLConnection is asynchoronous after all.
One does not need to do anything special to use NSURLConnection, it's a straightforward part of the Foundation framework. No special compiler settings required. No initialization before use. Nothing. Please don't start blindly trying stuff like bringing in CoreServices.Framework.
As sending the request synchronously works, there must be something wrong with your handling of the asynchronous aspect. It could be:
The runloop is not running in NSDefaultRunLoopMode so the connection is unable to schedule itself.
Some other part of your code is calling -cancel on the connection before it has a chance to load.
You are managing to deallocate the connection before it has a chance to load.
Real problem
Ah, in fact I've just realised what's going on. You are calling:
-[NSApp runModalForWindow:]
Read the description of what this method does. It's not running the run loop like NSURLConnection expects. I'd say that really, you don't want to be presenting a window quite like this while running a URL connection for it.
I'd also suggest that you implement the -connection:didReceiveResponse: delegate method too. You want to check here that the server is returning the expected status code.
You say that you're using this in a modal dialog? A modal dialog puts the run loop into a different mode. You should be able to get this to work by scheduling it to run in the modal dialog run loop mode, in addition to the normal run loop mode. Try adding this line of code after you allocate connection in loadURL:
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSModalPanelRunLoopMode];
Hope that helps.
How do you know it isn't doing anything? Are there any error or warning messages during the compile? Are any error messages showing up on console when the program is running?
Have you tries setting breakpoints in your code and following through what you expect to be happening?