I have a method foo: that is called on a background thread. This method simply sends a request to a server, and, after data are retrieved, performs some calculations about those data and returns. In this case I prefer to use sendSynchronousRequest: because this method is convenient and it doesn't matter if the thread is blocked. However, the response contains a "Location" header field that will redirect to another page. I want to read the response to get those "Set-Cookie" header fields before redirection. It seems that the synchronous method does not allow me to.
I tried to use the asynchronous one and implement a NSURLConnectionDataDelegate, but the thread is finished before those methods of the delegate is called. (I suppose the way that Apple implements the asynchronous one is to perform those time-consuming works on a new thread)
Is there any way to solve this problem? Since performing an asynchronous request on the main thread may add complexity to my program.
The foo: method is kind of like this
- (Result *)foo
{
NSURLMutableRequest * request = blablabla;
//Do something to initialize the request
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
//Do something with the data
Result *result = [[Result alloc] init] autorelease];
//fill the result
return result;
}
You could use a Grand Central Dispatch semaphore to wait until the asynchronous request returns:
- (Result *)foo
{
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] init];
// set request's properties here
__block Result *result;
dispatch_semaphore_t holdOn = dispatch_semaphore_create(0);
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error)
{
// handle error
}
else
{
result = [[Result alloc] initWithData:data];
}
dispatch_semaphore_signal(holdOn);
}];
dispatch_semaphore_wait(holdOn, DISPATCH_TIME_FOREVER);
return result;
}
NOTE: This code requires iOS 4.0+ and ARC!
Look into [NSCondition] which enables you to wait and signal threads
Basically you allocate a NSCondition and in the block you'll have [condition wait]; which will cause the thread to wait. then, when the async operation is done, you call [condition signal]; which will signal the waiting thread to continue.
http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/NSCondition_class/Reference/Reference.html
You can create your own NSRunLoop and do your requests there. Stop the run loop once you're done with your requests.
Or if you are lazy like me and don't want to mess with run loops, just put your connection on the main thread:
dispatch_async(dispatch_get_main_queue(), ^(void){
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection start];
}
You can find a small and simple class that lets you do this on github. It provides two primary objects - a NSOperationsQueue manager and NSOperation subclasses designed to run background fetches. As was mentioned, if all you were doing was fetches, you could do that on the main thread. But if you want to do data processing too, then this project will let you do that in a completed method.
A nice property of the OperationsRunner class is that you can cancel operations at any time (when for instance the user taps the back button), and everything gets torn down quickly with no stalling or leaking.
However, if all you ever do is this one fetch and one process, then you could as others have said just fetch the data on the main thread, and once you have it then dispatch a "processing" block to one of the concurrent system threads, and when that processing is done, dispatch another message to the main thread telling you that the work is complete.
Related
I am using NSURLConnection to load data from a response. It works as it should, the delegate method connectionDidFinishLoading has the connection instance with the data I need. The problem is that I want to pass some information along with the request so that I can get it when the connection finishes loading:
User wants to share the content of a URL via (Facebook, Twitter,
C, D).
NSURLConnection is used to get the content of the URL
Once I have the content, I use the SL framework
SLComposeViewController:composeViewControllerForServiceType and need
to give it the service type
At this point I don't know what service the user selected in step 1. I'd like to send that with the NSURLConnection.
Can I extend NSURLConnection with a property for this? That seems very heavy-handed. There must be a "right way" to do this.
Many Thanks
Assuming you don't need the delegate-based version of the NSURLConnection process for some other reason, this is a good use case for the block-based version:
- (void)shareContentAtURL:(NSURL *)shareURL viaService:(NSString *)service
{
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:shareURL];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] == 0 && error == nil) {
// handle empty response
} else if (error != nil) {
// handle error
} else {
// back to the main thread for UI stuff
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// do whatever you do to get something you want to post from the url content
NSString *postText = [self postTextFromData:data];
// present the compose view
SLComposeViewController *vc = [SLComposeViewController composeViewControllerForServiceType:service];
[vc setInitialText:postText];
[self presentViewController:vc animated:YES];
}];
}
}];
}
Since blocks can capture variables from their surrounding scope, you can just use whatever context you already had for the user's choice of service inside the NSURLConnection's completion block.
If you're still wed to the delegate-based NSURLConnection API for whatever reason, you can always use an ivar or some other piece of state attached to whatever object is handling this process: set self.serviceType or some such when the user chooses a service, then refer back to it once you get your content from the NSURLConnectionDelegate methods and are ready to show a compose view.
You could check the URL property of an NSURLConnection instance and determine the service by parsing the baseURL or absoluteString property of the URL with something like - (ServiceType)serviceTypeForURL:(NSURL *)theURL;
All the NSURLConnectionDelegate methods pass the calling NSURLConnection object-so you could get it from
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
or
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
Currently I have about 4 to 5 different webView's which I am trying to load simultaneously, however I have come across the conclusion that only one loadRequest can load at a time. Although I have created custom class for each webview, the loadRequest, doesn't seams to get called, while if there is another loadRequest of a webview is being utilized.
Is there anyway to keep these calls in different Thread's to make it work or utilize in dispatch mechanism? Just trying to figure out if there is an alternative.
Thanks
use NSUrlConnection to preload the data, no threading
//for EACH url to load
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:tweet[#"profile_image_url"]]];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue] // one should ideally use a different queue here to free main thread and ONLY do the imageView.image setting in Main thread
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if(!error) {
//load webview! with Data maybe!
}
}];
Since you seem to have only 4 or 5 web views, it is not that inefficient to call them on a dispatch queue. If there are 100's of requests, then you need to think of some strategies though.
NSArray *myWebSites = [NSArray arrayWithObjects:#"www.cnn.com",#"www.fox.com",....,nil];
for(url in myWebsites){
dispatch_async(dispatch_get_global_queue, ^{
// get the data from the url.
// do the display in main queue
dispatch_async(dispatch_get_mainqueue, ^{
//load the web view;
}
}
}
I am currently designing an iOS 5 iPhone app that will use a .NET RESTful Web Service to provide data updates. When the application is initially installed, it will connect to the WS to download all the data in a JSON format. Thereafter, it will only perform updates. The WS provides a POST method for each table as GetAllTableRecords() and GetLastUpdatedTableRecords().
I am using iOS 5 and I've got the NSURLConnection and JSON serialization/deserialization working correctly with native libraries. Each WS POST method call is currently residing in its own Obj-C class with all of the delegate methods. Additionally, each class handles the local datastore inserts and updates.
Each NSURLConnection is asynchronous and all of the WS calls are driven off of button events from view controllers.
My questions are:
Is this the right setup in terms of code encapsulation and reuse?
How do I handle making multiple WS calls while keeping the user
informed via the UI?
Currently there are two tables to download. This means the app will call the WS twice to get the initial data and twice again during each refresh. I know that since each NSURLConnection is asynchronous, the connection will make the request but the UI will continue on while the delegate handles the data download. I've done some research into GCD and NSOperation/Queue but I don't know enough about either one to code a solution or know if that's even a correct solution.
Any insight would be most helpful!
Edit #1: What about providing real time updates back to the UI? The Mint app does something similar when updating transactions and accounts. They have a little status bar that pops up at the bottom while requests are made.
Edit #2: Ok, I believe I've made some progress. We are using Story Boards and the entry point is the Login View/Controller. When the login button is clicked, a NSURLConnection is made to the webservice. If the response status code is 200 in connectionDidFinishLoading:(NSURLConnection *)connection, a segue is performed to go the Data Sync View. The purpose of this view is to either initialize or update the database while providing feedback to the user. Either updating or initializing requires two additional web service calls.
Here's my DataSyncView.m:
#synthesize pvStatus, lbStatus;
// pvStatus = progress indicator
// lbStatus = label
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self StartDataSync];
}
- (void)StartDataSync
{
[lbStatus setText:#"Syncing data..."];
[pvStatus setProgress:0.0f];
// TODO: Determine if database is popuplated
[self PerformInitialSync];
// Next screen
[self performSegueWithIdentifier:#"SegueFromSync" sender:self];
}
// Populates data store will data from web service
- (void)PerformInitialSync
{
// Kicks off a series of synchronous requests
[self DownloadAllEmployeeDataA];
}
- (void)DownloadAllDataA
{
// Dictonary holds POST values
NSMutableDictionary *reqDic = [NSMutableDictionary dictionary];
// Populate POST key/value pairs
[reqDic setObject:passWord forKey:#"Password"];
[reqDic setObject:userName forKey:#"UserName"];
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:reqDic options:NSJSONWritingPrettyPrinted error:&error];
// Convert dictionary to JSON
NSString *requestJSON = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
// Declare Webservice URL, request, and return data
NSURL *url = [[NSURL alloc] initWithString:#"http://wsurl/getalla"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
NSData *postData = [NSData dataWithBytes:[requestJSON UTF8String] length:[requestJSON length]];
// Build the request
[request setHTTPMethod:#"POST"];
[request setValue:[NSString stringWithFormat:#"%d", [postData length]] forHTTPHeaderField:#"Content-Length"];
[request setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:postData];
[request setCachePolicy:NSURLRequestUseProtocolCachePolicy];
[request setTimeoutInterval:60.0];
NSURLResponse *response;
[lbStatus setText:#"Downloading employee data..."];
[pvStatus setProgress:0.1f];
// Make the response
NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
// If return data received
if(returnData)
{
// Get the response and check the code
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
int code = [httpResponse statusCode];
// Check to make sure successful code
if (code == 200)
{
// Convert JSON objects to Core Data Entity
// Update UIProgressView and Label
// Call next WS call
[self DownloadAllEmployeeDataA];
}
}
}
- (void)DownloadAllDataB
{
// Same code as above but with different URL and entity
}
My problem that I am having is this: The UIProgressView and Label are not updating as the calls are being made. As I stated before, I don't even know if this is the best way to make these calls. It doesn't appear that I'm blocking the main thread but I could be wrong. Again, I'll pose the question: what is the best way to make multiple url calls while keeping the UI updated on the progress? Thanks!!!
// Make the response
NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
In your question you said you made asynchronous load of url request. But in the above line of code you are making a synchronous request ?
Is this the right setup in terms of code encapsulation and reuse?
Looking at your code you are not adhering to MVC. Your View
Controller shouldn't manage loading URL connections. You can create a
class that does that and using delegates inform the view controller
whether data is downloaded or failed to download,etc.
How do I handle making multiple WS calls while keeping the user informed via the UI?
If you want to make concurrent URL Connections then use NSOperation
and NSOperationQueue. Try avoiding GCD ( Refer to WWDC 2010 Session
208 ).
My problem that I am having is this: The UIProgressView and Label are not updating as the calls are being made.
You are making a Synchronous URL request on main thread. As per your
code UIProgressView shouldn't update.
Refer URL Loading System Programming Guide
Another comment I have is your method names, start method name with small letter. Rest of it looks fine. Coding Guidelines for Cocoa
I detach a NSThread say "thread2" and then call the NSUrlConnection class to get an xml from the remote server. Even it is not useful but i want to do that.
Now my problem is the thread2 does wait for the delegate methods response. I want that delegate methods should be called on that thread2 and thread wait for the response.
Is it possible or not. If yes then how.
You can use the synchronous method like so (assume you are dispatching the new thread to this method, or wrap this in a GCD call to a global queue):
- (void)threadDispatchMethod
{
NSError* error = nil;
NSData* result = nil;
NSURLResponse* response = nil;
result = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if( result ) {
// do wonderful things
} else {
// cry :'(
}
}
Our iPhone app code currently uses NSURLConnection sendSynchronousRequest and that works fine except we need more visibility into the connection progress and caching so we're moving to an async NSURLConnection.
What's the simplest way to wait for the async code to complete? Wrap it in a NSOperation/NSOperationQueue, performSelector..., or what?
Thanks.
I'm answering this in case anyone else bumps into the issue in the future. Apple's URLCache sample code is a fine example of how this is done. You can find it at:
iOS Developer Library - URLCache
As John points out in the comment above - don't block/wait - notify.
To use NSURLConnection asynchronously you supply a delegate when you init it in initWithRequest:delegate:. The delegate should implement the NSURLConnection delegate methods. NSURLConnection processing takes place on another thread but the delegate methods are called on the thread that started the asynchronous load operation for the associated NSURLConnection object.
Apart from notifications mentioned prior, a common approach is to have the class that needs to know about the URL load finishing set itself as a delegate of the class that's handling the URL callbacks. Then when the URL load is finished the delegate is called and told the load has completed.
Indeed, if you blocked the thread the connection would never go anywhere since it works on the same thread (yes, even if you are using the asynch methods).
I ran into this because our app used NSURLConnection sendSynchronousRequest in quite a few places where it made sense, like having some processing occurring on a background thread occasionally needing extra data to complete the processing. Something like this:
// do some processing
NSData * data = someCachedData;
if (data = nil) {
data = [NSURLConnection sendSynchronousRequest....]
someCachedData = data;
}
// Use data for further processing
If you have something like 3 different places in the same flow that do that, breaking it up into separate functions might not be desirable(or simply not doable if you have a large enough code base).
At some point, we needed to have a delegate for our connections(to do SSL certificate pinning) and I went trolling the internet for solutions and everything was of the form: "just use async and don't fight the framework!". Well, sendSynchronousRequest exists for a reason, this is how to reproduce it with an underlying async connection:
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse *__autoreleasing *)response error:(NSError *__autoreleasing *)error
{
static NSOperationQueue * requestsQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
requestsQueue = [[NSOperationQueue alloc] init];
requestsQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
});
NSCondition * waitLock = [NSCondition new];
[waitLock lock];
__block NSError * returnedError;
__block NSURLResponse * returnedResponse;
__block NSData * returnedData;
__block BOOL done = NO;
[NSURLConnection sendAsynchronousRequest:request
queue:requestsQueue
completionHandler:^(NSURLResponse * response, NSData * data, NSError * connectionError){
returnedError = connectionError;
returnedResponse = response;
returnedData = data;
[waitLock lock];
done = YES;
[waitLock signal];
[waitLock unlock];
}];
if (!done) {
[waitLock wait];
}
[waitLock unlock];
*response = returnedResponse;
*error = returnedError;
return returnedData;
}
Posted here in case anyone comes looking as I did.
Note that NSURLConnection sendAsynchrounousRequest can be replaced by whatever way you use to send an async request, like creating an NSURLConnection object with a delegate or something.