I am trying to create an xls sheet programmatically. To fill the sheet, I am making the multiple NSURLConnection around 100. Right now, my approach is :
Make a connection and store the data into an array . This array has 100 objects.
Now take the first object and call the connection . Store the data. And make the second connection with 2nd object in the array. This continues till the last object in the array.
It takes on average 14 seconds to finish the 100 connections. Is there any way to implement the NSURLConnection to get the response in a faster way?
Till yesterday I followed the basic approach like:
Declaring the properties:
#property (nonatomic,strong) NSURLConnection *getReportConnection;
#property (retain, nonatomic) NSMutableData *receivedData;
#property (nonatomic,strong) NSMutableArray *reportArray;
Initializing the array in viewDidLoad:
reportArray=[[NSMutableArray alloc]init];
Initializing the NSURLConnection in a button action :
/initialize url that is going to be fetched.
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"****/%#/crash_reasons",ID]];
//initialize a request from url
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request addValue:tokenReceived forHTTPHeaderField:#"**Token"];
[request setHTTPMethod:#"GET"];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
//initialize a connection from request
self.getReportConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
Processing the received data:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData*)data{
if (connection==_getVersionConnection) {
[self.receivedData_ver appendData:data];
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *e = nil;
NSData *jsonData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:jsonData options: NSJSONReadingMutableContainers error: &e];
[JSON[#"app_versions"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (![obj[#"id"] isEqual:[NSNull null]] && ![reportArray_ver containsObject:obj[#"id"]]) {
[reportArray_ver addObject:obj[#"id"]];
}
NSLog(#"index = %lu, Object For title Key = %#", (unsigned long)idx, obj[#"id"]);
}];
if (JSON!=nil) {
UIAlertView *alert=[[UIAlertView alloc]initWithTitle:#"Version Reports succesfully retrieved" message:#"" delegate:self cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alert show];
}
}
}
Calling the another connection after one finishes:
// This method is used to process the data after connection has made successfully.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
if (connection==getReportConnection) {
//check and call the connection again
}
}
And today, I tried the NSURLConnection with sendAsync to fire all the connections one after other using loop,and it worked pretty well.
self.receivedData_ver=[[NSMutableData alloc]init];
__block NSInteger outstandingRequests = [reqArray count];
for (NSString *URL in reqArray) {
NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:10.0];
[request setHTTPMethod:#"GET"];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *connectionError) {
[self.receivedData appendData:data]; //What is the use of appending NSdata into Nsmutable data?
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *e = nil;
NSData *jsonData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:jsonData options: NSJSONReadingMutableContainers error: &e];
NSLog(#"login json is %#",JSON);
[JSON[#"app_versions"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (![obj[#"id"] isEqual:[NSNull null]] && ![reportArray_ver containsObject:obj[#"id"]]) {
[reportArray_ver addObject:obj[#"id"]];
}
NSLog(#"index = %lu, Object For title Key = %#", (unsigned long)idx, obj[#"id"]);
}];
outstandingRequests--;
if (outstandingRequests == 0) {
//all req are finished
UIAlertView *alert=[[UIAlertView alloc]initWithTitle:#"Version Reports succesfully retrieved" message:#"" delegate:self cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alert show];
}
}];
}
This time it took half the time to complete the 100 requests than the old procedure, Is there any faster way exists other than the asynReq?.What is the best scenario to use NSURLconnection and NSURLConnection with asyncReq?
A couple of observations:
Use NSURLSession rather than NSURLConnection (if you are supporting iOS versions of 7.0 and greater):
for (NSString *URL in URLArray) {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
// configure the request here
// now issue the request
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// check error and/or handle response here
}];
[task resume];
}
If you absolutely have to issue 100 requests, then issue them concurrently like your sendAsynchronousRequest implementation (or my dataTaskWithRequest), not sequentially. That's what achieves the huge performance benefit.
Note, though, that you have no assurances that they'll completely in the order that you issued them, so you will want to use some structure that supports that (e.g. use NSMutableDictionary or pre-populate the NSMutableArray with placeholders so you can simply update the entry at a particular index rather than adding an item to the array).
Bottom line, be aware that they may not finish in the same order as requested, so make sure you handle that appropriately.
If you keep 100 separate requests, I'd suggest that you test this on a really slow network connection (e.g. use the Network Link Conditioner to simulate really bad network connection; see NSHipster discussion). There are problems (timeouts, UI hiccups, etc.) that only appear when doing this on slow connection.
Rather than decrementing a counter of number of pending requests, I'd suggest using dispatch groups or operation queue dependencies.
dispatch_group_t group = dispatch_group_create();
for (NSString *URL in URLArray) {
dispatch_group_enter(group);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
// configure the request here
// now issue the request
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// check error and/or handle response here
// when all done, leave group
dispatch_group_leave(group);
}];
[task resume];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// do whatever you want when all of the requests are done
});
If possible, see if you can refactor the web service so you are issuing one request that returns all of the data. If you're looking for further performance improvement, that's probably the way to do it (and it avoids a lot of complexities involved when issuing 100 separate requests).
BTW, if you use delegate based connection, like you did in your original question, you should not be parsing data in didReceiveData. That should only be appending data to a NSMutableData. Do all of the parsing in connectionDidFinishLoading delegate method.
If you go to block-based implementation, this issue goes away, but just an observation on your code snippets.
Using sendAsynchronous is a great way to improve code organization. I'm sure with some careful scrutiny, we could improve the speed at the margin, but the way to noticeably improve speed is to not make 100 requests.
If the response bodies are small, create an endpoint that answers a conjunction of the results.
If the response bodies are large, then you're requesting more data than the user needs at the moment. Hold up the UI only on what user needs to see, and get the rest silently (... or, maybe better than silently, lazily).
If you don't control the server, and the response bodies are small, and the user needs all or most of to carry on with the app, then you can start working on performance at the margins and UI tricks to amuse user while the app works, but usually one of those constraints -- usually the latter -- can be relaxed.
Related
I need to use NSURLSession to make network calls. On the basis of certain things, after I receive the response, I need to return an NSError object.
I am using semaphores to make the asynchronous call behave synchronously.
The problem is, the err is set properly inside call, but as soon as semaphore ends (after
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
), the err becomes nil.
Please help
Code:
-(NSError*)loginWithEmail:(NSString*)email Password:(NSString*)password
{
NSError __block *err = NULL;
// preparing the URL of login
NSURL *Url = [NSURL URLWithString:urlString];
NSData *PostData = [Post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
// preparing the request object
NSMutableURLRequest *Request = [[NSMutableURLRequest alloc] init];
[Request setURL:Url];
[Request setHTTPMethod:#"POST"];
[Request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[Request setHTTPBody:PostData];
NSMutableDictionary __block *parsedData = NULL; // holds the data after it is parsed
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:Request completionHandler:^(NSData *data, NSURLResponse *response1, NSError *err){
if(!data)
{
err = [NSError errorWithDomain:#"Connection Timeout" code:200 userInfo:nil];
}
else
{
NSString *formattedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#", formattedData);
if([formattedData rangeOfString:#"<!DOCTYPE"].location != NSNotFound || [formattedData rangeOfString:#"<html"].location != NSNotFound)
{
loginSuccessful = NO;
//*errorr = [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil];
err = [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil];
}
else
{
parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&err];
NSMutableDictionary *dict = [parsedData objectForKey:#"User"];
loginSuccessful = YES;
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// but have the thread wait until the task is done
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return err;
}
Rob's answer tells you how to do it right, but not what mistake you made:
You have two variables named err, which are totally unrelated. It seems that you haven't turned on some important warnings, otherwise your code wouldn't even have compiled.
The parameter err that is passed to your completion block is the error from the URL request. You replace it without thinking with a timeout error - so the true error is now lost. Consider that timeout is not the only error.
But all the errors that you set only set the local variable err which was passed to you in the completion block; they never touch the variable err in the caller at all.
PS. Several serious errors in your JSON handling. JSON can come in UTF-16 or UTF-32, in which case formattedData will be nil and you incorrectly print "Server Issue". If the data isn't JSON there is no guarantee that it contains DOCTYPE or html, that test is absolute rubbish. Your user with the nickname JoeSmith will hate you.
Passing NSJSONReadingAllowFragments to NSJSONSerialization is nonsense. dict is not mutable; if you try to modify it your app will crash. You don't check that the parser returned a dictionary, you don't check that there is a value for the key "User", and you don't check that the value is a dictionary. That's lots of ways how your app can crash.
I would suggest cutting the Gordian knot: You should not use semaphores to make an asynchronous method behave synchronously. Adopt asynchronous patterns, e.g. use a completion handler:
- (void)loginWithEmail:(NSString *)email password:(NSString*)password completionHandler:(void (^ __nonnull)(NSDictionary *userDictionary, NSError *error))completionHandler
{
NSString *post = ...; // build your `post` here, making sure to percent-escape userid and password if this is x-www-form-urlencoded request
NSURL *url = [NSURL URLWithString:urlString];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
// [request setValue:postLength forHTTPHeaderField:#"Content-Length"]; // not needed to set length ... this is done for you
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"]; // but it is best practice to set the `Content-Type`; use whatever `Content-Type` appropriate for your request
[request setValue:#"text/json" forHTTPHeaderField:#"Accept"]; // and it's also best practice to also inform server of what sort of response you'll accept
[request setHTTPBody:postData];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *err) {
if (!data) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, [NSError errorWithDomain:#"Connection Timeout" code:200 userInfo:nil]);
});
} else {
NSError *parseError;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
if (parsedData) {
NSDictionary *dict = parsedData[#"User"];
completionHandler(dict, nil);
} else {
completionHandler(nil, [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil]);
}
});
}
}];
[task resume];
}
And then call it like so:
[self loginWithEmail:userid password:password completionHandler:^(NSDictionary *userDictionary, NSError *error) {
if (error) {
// do whatever you want on error here
} else {
// successful, use `userDictionary` here
}
}];
// but don't do anything reliant on successful login here; put it inside the block above
Note:
I know you're going to object to restoring this back to asynchronous method, but it's a really bad idea to make this synchronous. First it's a horrible UX (the app will freeze and the user won't know if it's really doing something or whether it's dead) and if you're on a slow network you can have all sorts of problems (e.g. the watchdog process can kill your app if you do this at the wrong time).
So, keep this asynchronous. Ideally, show UIActivityIndicatorView before starting asynchronous login, and turn it off in the completionHandler. The completionHandler would also initiate the next step in the process (e.g. performSegueWithIdentifier).
I don't bother testing for HTML content; it is easier to just attempt parse JSON and see if it succeeds or not. You'll also capture a broader array of errors this way.
Personally, I wouldn't return my own error objects. I'd just go ahead and return the error objects the OS gave to me. That way, if the caller had to differentiate between different error codes (e.g. no connection vs server error), you could.
And if you use your own error codes, I'd suggest not varying the domain. The domain should cover a whole category of errors (e.g. perhaps one custom domain for all of your app's own internal errors), not vary from one error to another. It's not good practice to use the domain field for something like error messages. If you want something more descriptive in your NSError object, put the text of the error message inside the userInfo dictionary.
I might suggest method/variable names to conform to Cocoa naming conventions (e.g. classes start with uppercase letter, variables and method names and parameters start with lowercase letter).
There's no need to set Content-Length (that's done for you), but it is good practice to set Content-Type and Accept (though not necessary).
You need to let the compiler know that you will be modifying err. It needs some special handling to preserve that beyond the life of the block. Declare it with __block:
__block NSError *err = NULL;
See Blocks and Variables in Blocks Programming Topics for more details.
I am working with an app which is todo list organizer, where user adds notes. I am using coredata DB to store the notes. As I am providing sync feature, I am parsing JSON data to server, and also getting JSON data from server.
I am using NSURLConnection API and its delegate functions
- (void)pushData
{
loop through the notes array and send notes 1 by one
[[request setValue:#"application/json;charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:jsonData];
m_dataPush = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
[m_dataPush start];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
Process response from server, save to core DB
and again pushData if any modified and again process the response
}
I call this API, on appEnterBackground and appBecomeActive, because, I want the data to updated on multiple devices.
The problems, which I am facing is that
1) When the notes are more, app is getting stuck, when we exit and open the app and start adding notes.
2) I tried using GCD, but then my NSURLConnection doesnot send me any response
Regards
Ranjit
Ranjit: Based on your comments in the different responses, I suspect you are sending the 1st request from the main thread. When you receive the 1st response, you process it in the background, and then send the 2nd request also from the background. The subsequent requests should be sent from the main thread
[self performSelectorOnMainThread:#selector(myMethodToOpenConnection:)
withObject:myObject
waitUntilDone:NO];
otherwise the thread exits before the delegate is called
You can use NSOperation Queue with NSURLConnection like this
//allocate a new operation queue
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//Loads the data for a URL request and executes a handler block on an
//operation queue when the request completes or fails.
[NSURLConnection
sendAsynchronousRequest:urlRequest
queue:queue
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *error) {
if ([data length] >0 && error == nil){
//process the JSON response
//use the main queue so that we can interact with the screen
NSString *myData = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(#"JSON data = %#", myData);
NSDictionary *myDict = [myData JSONValue];
}
}];
it will do all the processing in the background.
NSURLConnection provides a convenience method called sendAsynchronousRequest: completionHandler: that does the GCD work for you. You can tell it to run the completion handler on the main thread.
Using it, your code would get simpler as follows:
// place a declaration in your .h to make it public
- (void)pushDataWithCompletion:(void (^)(BOOL, NSError*))completion;
- (void)pushDataWithCompletion:(void (^)(BOOL, NSError*))completion
{
// setup your connection request...
[[request setValue:#"application/json;charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:jsonData];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// whatever you do on the connectionDidFinishLoading
// delegate can be moved here
if (!error) {
// did finish logic here, then tell the caller you are done with success
completion(YES, nil);
} else {
// otherwise, you are done with an error
completion(NO, error);
}
}];
}
Exactly what you pass back in the block depends on what the callers care about. It's common to make some aspect of the data you collected one of the block params.
EDIT - I left out the pointer notation (*) after NSError above.
Also, say you have an array of objects that needs to be processed by the server. This method is good for one call. To handle several, lets give it a parameter. Say that each note is an NSString *;
- (void)pushNote:(NSString *)note withCompletion:(void (^)(BOOL, NSError*))completion {
// Code is the same except it forms the request body using the note parameter.
}
If the real task is to do work for several notes, you need a method that calls this one repeatedly, then tells its caller that its done.
- (void)pushNotes:(NSArray *)notes withCompletion:(void (^)(BOOL, NSError*))completion {
// if there are no more notes, we are done
if (!notes.count) return completion(YES, nil);
NSString *nextNote = notes[0];
NSArray *remainingNotes = [notes subarrayWithRange:NSMakeRange(1, notes.count-1)];
[self pushNote:nextNote withCompletion:^(BOOL success, NSError*error) {
// if success, do the rest, or else stop and tell the caller
if (success) {
[self pushNotes:remainingNotes withCompletion:completion];
} else {
completion(NO, error);
}
}];
}
I have the following code to get data from server;
-(void)loginForFaceBook
{
GTMOAuth2ViewControllerTouch *viewController;
viewController = [[GTMOAuth2ViewControllerTouch alloc]
initWithScope:#"https://www.googleapis.com/auth/plus.me"
clientID:#"27615...6qdi60qjmachs.apps.googleusercontent.com"
clientSecret:#"Fs8A...u2PH"
keychainItemName:#"OAuth2 Sample:
Google+"
delegate:self
finishedSelector:#selector(viewController:finishedWithAuth:error:)];
[[self navigationController] pushViewController:viewController
animated:YES];
}
- (void)viewController:(GTMOAuth2ViewControllerTouch *)viewController
finishedWithAuth:(GTMOAuth2Authentication *)auth
error:(NSError *)error {
if (error != nil) {
// Authentication failed (perhaps the user denied access, or closed the
// window before granting access)
NSLog(#"Authentication error: %#", error);
NSData *responseData = [[error userInfo] objectForKey:#"data"]; //
kGTMHTTPFetcherStatusDataKey
if ([responseData length] > 0) {
// show the body of the server's authentication failure response
// NSString *str = [[NSString alloc] initWithData:responseData
// encoding:NSUTF8StringEncoding];
// NSLog(#"%#", str);
}
// self.auth = nil;
} else {
// NSString *authCode = [NSString alloc]in;
NSMutableURLRequest * request;
request = [[NSMutableURLRequest alloc] initWithURL:[NSURL
URLWithString:#"http://api.kliqmobile.com/v1/tokens"]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60] ;
NSLog(#"%#",auth);
NSLog(#"ho gya success %# :::: %# :::: %#", auth.accessToken,
auth.refreshToken, auth.code);
NSMutableURLRequest * response;
NSError * error;
request.URL = [NSURL URLWithString:#"http://api.kliqmobile.com/v1/tokens"];
NSString *post = [NSString stringWithFormat:#"
{\"token\":\"%#\",\"secret\":\"%#\",\"service\":\"%#\",\"handle\":\"%#\"}",
auth.accessToken,auth.code,#"google",nil];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding
allowLossyConversion:YES];
NSString *postLength = [NSString stringWithFormat:#"%d",[postData length]];
[request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[request setValue:#"application/x-www-form-urlencoded"
forHTTPHeaderField:#"Content-Type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:postData];
error = nil;
response = nil;
NSURLConnection *connection = [NSURLConnection connectionWithRequest:request
delegate:self];
[connection start];
}
I have implemented the NSURLConnection delegtes method and data is printing well like this
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
NSMutableURLRequest * response;
NSError * error;
NSLog(#"Did Receive Data %#", [[NSString alloc]initWithData:data
encoding:NSUTF8StringEncoding]);
NSMutableURLRequest * requestContacts;
requestContacts = [[NSMutableURLRequest alloc] initWithURL:[NSURL
URLWithString:#"http://api.kliqmobile.com/v1/contacts"]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60] ;
[requestContacts setHTTPMethod:#"GET"];
[requestContacts setAllHTTPHeaderFields:headers];
error = nil;
response = nil;
NSData* data1 = [NSURLConnection sendSynchronousRequest:requestContacts
returningResponse:&response error:&error];
NSLog(#"WE GET THE REQUIRED TOKAN DATA %# :: %# :: %#", [[NSString alloc]
initWithData:data1 encoding: NSASCIIStringEncoding], error ,response);
}
but after that my app get crashed and it is giving following error;
[NSHTTPURLResponse release]: message sent to deallocated instance 0xcb51070.
please suggest me how to do this.
A couple of thoughts:
What is the intent of your didReceiveData method? There are a bunch of issues here:
You really shouldn't be doing a synchronous network request in the middle of a NSURLConnectionDataDelegate method.
You shouldn't be doing synchronous requests at all, but rather do them asynchronously.
What is the connection between receiving data and your creation of this new request? You're not using the data in the request, so why do it here?
The typical pattern is:
The didReceiveResponse should instantiate a NSMutableData object in some class property.
The only function of didReceiveData should be to append the received data to the NSMutableData. Note, this method may be called multiple times before all the data is received.
In connectionDidFinishLoading, you should initiate any next steps that you take upon successful completion of the request. If you wanted to do start another asynchronous network request when the initial request is done, do that here.
In didFailWithError, you obviously handle any failure of the connection.
When you call connectionWithRequest, you should not use the start method. Only use start when you use initWithRequest:delegate:startImmediately: with NO for the startImmediately parameter. Otherwise the connection starts automatically for you and you're only starting it a second time.
Unrelated to your original question, but your creation of post string cannot be right. You're missing a parameter value. Even better, rather than creating JSON manually, use NSDictionary and then use NSJSONSerialization to make the NSData object containing the JSON from this dictionary. That's much safer:
NSDictionary *params = #{#"token" : auth.accessToken,
#"secret" : auth.code,
#"service" : #"google",
#"handle" : #""};
NSError *error;
NSData *postData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error];
Clearly, supply whatever you need for the handle value.
A tangential process-related observation, but I'm wondering if you're taking advantage of everything Xcode offers. For example, your declaration of response as a NSMutableURLRequest but then using that as a parameter to sendSynchronousRequest should have generated a compiler warning. The same thing is true with your stringWithFormat for your post string (my third point). That should have generated a warning, too.
Neither of these are immediately relevant, but I wonder if you are failing to heed any other compile-time warnings. These warnings are your best friend when writing robust code and I would recommend resolving all of them. To go a step further, you should also run the project through the static analyzer ("Analyze" on "Product" menu, or shift+command+B), and resolve anything it points out, too.
I have the following problem. I have a Model, called User. When the user now logins with Facebook, my app checks if the user exists already in the database. To not freeze the UI (since I'm coming from Android) I thought to use NSURLConnection sendAsynchronousRequest. What worked at first was the following:
My User Model had a method to do the whole task of the AsynchronousRequest and then when finished would set a variable to loading. Then other classes, could simply check with
while ( !user.loading ) if the Request was finished or not. The problem that came here to me, was, that now, I had to put this method in every Model. So instead of this, I created a new Class HTTPPost. This class now has the method that gets an NSDictionary passed and returns one. This works ALMOST. The problem I was now encountering is, that I couldn't really determine if the process was finished or not. So I started to create a new class called Globals and use global Variable loading. But the global variable is ALWAYS NO. So, what would be the best way to do this?
Here is my code:
This is where I check for the user and load it. resultDictionary is the NSDictionary where everything gets loaded in, but is always nil
[user loadModelFrom:[NSString stringWithFormat:#"WHERE facebookId='%#'", graphUser.id]];
NSLog(#"%#", user.resultDictionary);
if ( user.resultDictionary == nil ) {
NSLog(#"NIL");
} else {
NSLog(#"NOT NIL");
}
The problem now, is, that, since I'm sending an AsynchronousRequest, the resultDictionary is always nil. What I did before and worked was the following.
In my Model I had the HTTP Request and a variable named loading. Now I set loading to false until the response has been made into a NSDictionary
returnDict = [NSJSONSerialization JSONObjectWithData: [responseBody dataUsingEncoding:NSUTF8StringEncoding]
options: NSJSONReadingMutableContainers
error: &error];
But, then I had another problem. I had to do this in all my Models again... So I created a new Class that subclasses NSObject, that has the asynchronousRequest. This is the whole request
-(NSDictionary *)doHttpRequest:(NSDictionary *)postDict{
loading = NO;
__block NSDictionary *returnDict;
NSError *error;
NSString *jsonString;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:postDict
options:NSJSONWritingPrettyPrinted // Pass 0 if you don't care about the readability of the generated string
error:&error];
if (! jsonData) {
NSLog(#"Got an error: %#", error);
} else {
jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
NSURL *aUrl = [NSURL URLWithString:#"http://xx.xx-xx.xx/xx.xx"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:aUrl
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSString *authStr = [NSString stringWithFormat:#"%#:%#", #"xx", #"xx"];
NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
NSString *authValue = [NSString stringWithFormat:#"Basic %#", [authData base64EncodedString]];
[request setValue:authValue forHTTPHeaderField:#"Authorization"];
[request setValue:#"application/json" forHTTPHeaderField:#"Content-type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSString *responseBody = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
returnDict = [NSJSONSerialization JSONObjectWithData: [responseBody dataUsingEncoding:NSUTF8StringEncoding]
options: NSJSONReadingMutableContainers
error: &error];
}];
[queue waitUntilAllOperationsAreFinished];
loading = YES;
return returnDict;
}
As you can see I have now a variable called loading. It is a global variable. But somehow, the variable is always NO.
What would be the best way to do this? I hope I'm understandable, I'm new to Objective-C, and English isn't my native language.
UPDATE
I modified the code to look like a user provided here, but still not working!
HTTPPost.h
-(void)doHttpRequest:(NSDictionary *)postDict completion:(void(^)(NSDictionary *dict, NSError *error))completion {
__block NSDictionary *returnDict;
NSError *error;
NSString *jsonString;
NSString *authValue;
NSString *authStr;
NSData *jsonData;
NSData *authData;
NSURL *aUrl;
NSMutableURLRequest *request;
NSOperationQueue *queue;
jsonData = [NSJSONSerialization dataWithJSONObject:postDict
options:NSJSONWritingPrettyPrinted
error:&error];
if (! jsonData) {
NSLog(#"Got an error: %#", error);
} else {
jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
aUrl = [NSURL URLWithString:#"http://xx.xx-xx.com/xx.php"];
request = [NSMutableURLRequest requestWithURL:aUrl
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
queue = [[NSOperationQueue alloc] init];
authStr = [NSString stringWithFormat:#"%#:%#", #"xx", #"xx"];
authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
authValue = [NSString stringWithFormat:#"Basic %#", [authData base64EncodedString]];
[request setValue:authValue forHTTPHeaderField:#"Authorization"];
[request setValue:#"application/json" forHTTPHeaderField:#"Content-type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSString *responseBody = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
returnDict = [NSJSONSerialization JSONObjectWithData: [responseBody dataUsingEncoding:NSUTF8StringEncoding]
options: NSJSONReadingMutableContainers
error: &error];
if ( completion ) {
completion(returnDict, error);
}
}];
}
//User.h
[_httpPost doHttpRequest:_dbDictionary completion:^(NSDictionary *dict, NSError *error) {
NSLog(#"completed") // NEVER GETS FIRED
}];
It seems that you're trying to take an asynchronous process (sendAsynchronousRequest) , and make it behave like a synchronous process (i.e. you appear to want to wait for it). You should not do that. You should to embrace the asynchronous patterns rather than fighting them.
The sendAsynchronousRequest method has a completion block that specifies what you want to do when the request is done. Do not try to put the code after the block and (try to) wait for the block to complete, but rather put any of your code that is dependent upon the completion of the network request inside the completion block, or have the completion block call your code.
A common way would be to give your own methods their own completion blocks and then call those blocks in the completionHandler of sendAsynchronousRequest, something like:
- (void)performHttpRequest:(NSDictionary *)postDict completion:(void (^)(NSDictionary *dictionary, NSError *error))completion
{
// prepare the request
// now issue the request
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error) {
if (completion)
completion(data, error);
} else {
NSString *responseBody = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
returnDict = [NSJSONSerialization JSONObjectWithData:data
options: NSJSONReadingMutableContainers
error: &error];
if (completion)
completion(returnDict, error);
}];
}
Now, when you want to perform your request, you simply do:
[self performHttpRequest:someDictionary completion:^(NSDictionary *dictionary, NSError *error) {
if (error) {
// ok, handle the error here
} else {
// ok, use the `dictionary` results as you see fit here
}
];
Note, the method that calls this performHttpRequest (let's imagine you called it from loadModelFrom ) now behaves asynchronously, itself. So you might want to employ this completion-block pattern again, e.g. adding your own completion block parameter to loadModelFrom, and then invoke that block in the completion handler loadModelFrom passes to performHttpRequest.
But hopefully you get the idea: Never try to wait for a completion block, but rather just put inside that block anything you want it to do when its done. Whether you use AFNetworking (which I'd advise), or continue to use sendAsynchronousRequest, this is a very useful pattern with which you should become familiar.
Update:
The revised code sample (largely) works great for me. Seeing your revised question, a couple of observations:
I am not familiar with this base64EncodedString method. In iOS 7, there is the native base64EncodedStringWithOptions method (or for earlier iOS versions use base64Encoding). Or are you using a third party base-64 NSData category?
There's no point in creating jsonString, only to then convert it back to a NSData. Just use jsonData in your request.
The same is true with responseBody: Why convert to string only to convert back to NSData?
There's no point in having returnDict to be defined as __block outside the sendAsynchronousRequest block. Just define it inside that block and the __block qualifier is then no longer necessary.
Why create a NSOperationQueue for the completionHandler of sendAsynchronousRequest? Unless I'm doing something really slow that merits running on a background queue, I just use [NSOperationQueue mainQueue], because you invariably want to update the app's model or UI (or both), and you want to do that sort of stuff on the main queue.
The request still runs asynchronously but the queue parameter just specifies which queue the completion block will run on.
By the way, in sendAsynchronousRequest, you aren't checking to see if the request succeeded before proceeding with JSONObjectWithData. If the request failed, you could theoretically be losing the NSError object that it returned. You really should check to make sure the request succeeded before you try to parse it.
Likewise, when you originally dataWithJSONObject the parameters in postDict, you really should check for success, and if not, report the error and quit.
I notice that you're using the NSJSONReadingMutableContainers option. If you really need a mutable response, I'd suggest making that explicit in your block parameters (replacing all the NSDictionary references with NSMutableDictionary). I assume you don't really need it to be mutable, so I therefore recommend removing the NSJSONReadingMutableContainers option.
Likewise, when creating the JSON, you don't need to use the NSJSONWritingPrettyPrinted option. It only makes the request unnecessary larger.
Combining all of this, that yields:
-(void)performHttpRequest:(NSDictionary *)postDict completion:(void(^)(NSDictionary *dict, NSError *error))completion {
NSError *error;
NSString *authValue;
NSString *authStr;
NSData *jsonData;
NSData *authData;
NSURL *aUrl;
NSMutableURLRequest *request;
jsonData = [NSJSONSerialization dataWithJSONObject:postDict options:0 error:&error];
if (!jsonData) {
if (completion)
completion(nil, error);
return;
}
aUrl = [NSURL URLWithString:#"...."];
request = [NSMutableURLRequest requestWithURL:aUrl
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
authStr = [NSString stringWithFormat:#"%#:%#", #"xx", #"xx"];
authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
if ([authData respondsToSelector:#selector(base64EncodedStringWithOptions:)])
authValue = [NSString stringWithFormat:#"Basic %#", [authData base64EncodedStringWithOptions:0]];
else
authValue = [NSString stringWithFormat:#"Basic %#", [authData base64Encoding]]; // if only supporting iOS7+, you don't need this if-else logic and you can just use base64EncodedStringWithOptions
[request setValue:authValue forHTTPHeaderField:#"Authorization"];
[request setValue:#"application/json" forHTTPHeaderField:#"Content-type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:jsonData];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (!data) {
if (completion)
completion(nil, error);
return;
}
NSError *parseError = nil;
NSDictionary *returnDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
if (completion) {
completion(returnDict, parseError);
}
}];
}
And if this is being called from another method that needs to handle the fact that this is happening asynchronously, then it would employ a completion block pattern, too:
- (void)authenticateUser:(NSString *)userid password:(NSString *)password completion:(void (^)(BOOL success))completion
{
NSDictionary *dictionary = #{ ... };
[self performHttpRequest:dictionary completion:^(NSDictionary *dict, NSError *error) {
if (error) {
completion(NO);
return;
}
// now validate login by examining resulting dictionary
BOOL success = ...;
// and call this level's completion block
completion(success);
}];
}
Then the view controller might access that method with something like:
// maybe add UIActivityIndicatorView here
[self.userModel authenticateUser:self.userTextField.text password:self.passwordTextField.text completion:^(BOOL success) {
// remove UIActivityIndicatorView here
if (success) {
// do whatever you want if everything was successful, maybe segue to another view controller
} else {
// show the user an alert view, letting them know that authentication failed and let them try again
}
}];
After seeing you adding specific code to handle request and its responses, I would point out that you should try using AFNetworking. It abstracts out lots of boiler plate code.
As you mentioned, you are new to obj-c, it may take some time to understand AFNetworking but in long run, it will save you lots of headache. Plus it is one of the widely used open source for network related stuff.
I hope this would be helpful.
If you want to wait for a request, then you should not use sendAsynchronousRequest.
Use sendSynchonousRequest instead. That's where it's made for:
NSURLResponse *response;
NSError * error;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
But, the UI is blocked when the synchronous call is made. I doubt if that is what you want.
I currently have a screen with 2 tables. I'm getting the data synchronously and putting it on the screen. Code looks something like:
viewController.m
DBAccess_Error_T = [getList:a byCompanyID:1];
DBAccess_Error_T = [getList:b byCompanyID:2];
[self putListAOnScreen];
[self putListBOnScreen];
DBAccess.m
+ (DBAccess_Error_T)getList:(NSMutableArray*)a byCompanyID:(NSInteger)cID
{
// Pack this up in JSON form
[self queryDB:postData];
// Unpack and put it into variable a
}
+ (id)queryDB:(id)post
{
// Send request
// Get back data
}
I'm now trying to switch this over to async and I'm struggling. It's been hard even with website tutorials and documentations.
Since all of my database utilities are in separate files from the viewControllers, I'm not sure how I can use the didReceiveData and didReceiveResponse handlers. Also, since I have 2 arrays to fill for my 2 tables, how do I distinguish the difference in didReceiveData?
Instead, what I'm trying to do now is use sendAsynchronousRequest, but it seems I need to create an unpack function for every send function...let me know if I'm way off here...it looks something like:
viewController.m stays the same
DBAccess.m
+ (DBAccess_Error_T)getList:(NSMutableArray*)a byCompanyID:(NSInteger)cID
{
NSDictionary *post = /*blah blah*/
[self queryDB:post output:(a)];
}
+ (id)queryDB:(id)post output:(id)output
{
NSError *error;
NSData *jsonPayload = [NSJSONSerialization dataWithJSONObject:post options:NSJSONWritingPrettyPrinted error:&error];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[theRequest setHTTPMethod:#"POST"];
[theRequest setHTTPBody:jsonPayload];
[NSURLConnection sendAsynchronousRequest:request
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *error)
{
if ([data length] >0 && error == nil)
{
[self unpackDataForList:output data:data]; // This function needs to be different depending on which function called queryDB...the data will be unpacked in a different way
}
}
}
+ (void)unpackDataForList:(id)output data:(NSData*)data
{
// Do my unpacking here and stick it into 'output'.
}
How can I call a different unpackData function? are function pointers the right way to do this? Is this approach way off? Any tips would be greatly appreciated!
Have you ever looked at ASIHTTPRequest? It makes your life a lot easier by allowing you to use blocks. Here's an example of how to make an asynchronous request:
- (IBAction)grabURLInBackground:(id)sender
{
NSURL *url = [NSURL URLWithString:#"http://allseeing-i.com"];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
// Use when fetching text data
NSString *responseString = [request responseString];
// Use when fetching binary data
NSData *responseData = [request responseData];
}];
[request setFailedBlock:^{
NSError *error = [request error];
}];
[request startAsynchronous];
}
You can find more information here:
http://allseeing-i.com/ASIHTTPRequest/