I have an implementation of a RESTful API in a class called WebServices. I call methods in this class from other classes within my app and would like to perform actions upon successful completion of a web service. I have a completion block as a part of my method headers, but am not sure if I am using them correctly as the app never seems to reach inside the completion part of the method call. This is how I am calling my method:
[services callUnregisterForPushNotifications:savedAccessToken pushToken:savedPushToken completionBlock:^(NSMutableArray *resultsArray) {
NSLog(#">>> COMPLETE! <<<");
[self.loadingView removeFromSuperview];
}];
And the method I am calling looks like this:
- (void)callUnregisterForPushNotifications:(NSString *)accessToken
pushToken:(NSString *)pushToken
completionBlock:(void (^)(NSMutableArray *resultsArray))completion{
NSLog(#"UNREGISTER FOR PUSH CALLED!");
NSLog(#"PUSH TOKEN %#", pushToken);
NSString *appendUrl = #"alerts/unregisterpush/";
NSLog(#"APPEND URL %#",appendUrl);
NSURL *unregisterUrl = [NSURL URLWithString:[NSString stringWithFormat:#"%#%#", BaseURLString, appendUrl]];
NSLog(#"UNREGISTER URL: %#",unregisterUrl);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:unregisterUrl
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30.0];
[request setHTTPMethod:#"PUT"];
NSString *appendToken = [NSString stringWithFormat:#"Bearer %#", accessToken];
NSLog(#"TOKEN: %#",appendToken);
[request addValue:appendToken forHTTPHeaderField:#"Authorization"];
[request addValue:#"application/json, text/plain, */*" forHTTPHeaderField:#"Accept"];
[request addValue:#"application/json;charset=UTF-8" forHTTPHeaderField:#"Content-Type"];
NSString *postString = [NSString stringWithFormat:#"{\"Guid\":\"%#\"}",pushToken];
NSLog(#"POST STRING: %#",postString);
[request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
NSLog(#"REQUEST %#",request);
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog(#"UNREGISTER PUSH NOTIFICATIONS RESPONSE: %#", response);
NSLog(#"UNREGISTER PUSH NOTIFICATIONS ERROR: %#", error);
NSLog(#"UNREGISTER PUSH NOTIFICATIONS DATA: %#", data);
NSData *_data = data;// ... whatever
NSMutableString *_string = [NSMutableString stringWithString:#""];
for (int i = 0; i < _data.length; i++) {
unsigned char _byte;
[_data getBytes:&_byte range:NSMakeRange(i, 1)];
if (_byte >= 32 && _byte < 127) {
[_string appendFormat:#"%c", _byte];
} else {
[_string appendFormat:#"[%d]", _byte];
}
}
NSLog(#"UNREGISTER PUSH RESPONSE: %#", _string);
id obj= [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!obj) {
//NSLog(#"REGISTER PUSH NOTIFICATIONS ERROR: %#", error);
} else {
//NSLog(#"REGISTER PUSH NOTIFICATIONS DATA: %#", obj);
//self.accessToken = [obj valueForKey:#"access_token"];
//NSLog(#"ACCESS TOKEN: %#",self.accessToken);
}
}];
}
Any advice / input would be appreciated, thanks in advance!
In your else block you should call the completion handler like so:
// [code omitted for brevity]
NSLog(#"UNREGISTER PUSH RESPONSE: %#", _string);
id obj= [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!obj) {
//NSLog(#"REGISTER PUSH NOTIFICATIONS ERROR: %#", error);
} else {
completionHandler(resultsArray); // add this line to actually call the completion block passed in
//NSLog(#"REGISTER PUSH NOTIFICATIONS DATA: %#", obj);
//self.accessToken = [obj valueForKey:#"access_token"];
//NSLog(#"ACCESS TOKEN: %#",self.accessToken);
}
}];
Also make sure to pass in the actual array instead of the resultsArray parameter I passed.
What you are basically doing is passing in a function (or "block") which does not know when to execute (you'll have to do that yourself after any asynchronous tasks have finished). So you passed in the block from your calling method:
[services callUnregisterForPushNotifications:savedAccessToken pushToken:savedPushToken completionBlock:^(NSMutableArray *resultsArray) {
NSLog(#">>> COMPLETE! <<<");
[self.loadingView removeFromSuperview];
}];
The block of code contained in the curly braces is passed on to the function callUnregisterForPushNotifications:pushToken:completionHandler: and assigned to the completionHandler parameter which now contains the block of code you passed in when initially calling the method. The method that receives the completionHandler block is responsible for calling it once the asynchronous tasks (network requests) are finished (as you can see in the first snippet I posted).
completionHandler(resultsArray);
That way, once your request has finished (the else block) you are ready to execute the completion block that was passed in. That will effectively mean "Execute the block of code I passed in earlier because now we have the data from the network operation."
Related
I am struggling with a response object from one of my API calls. I receive it fine, but if I try to perform a count or valueForKey operation on the object, I get a "Unrecognized selector sent to instance" error. I have a feeling I am not correctly decoding the response object, any input would be greatly appreciated!
Method for the API call:
- (void)callRegisterAccount:(NSString *)email
password:(NSString *)password
confirmPassword:(NSString *)confirmPassword
completionBlock:(void (^)(NSMutableArray *resultsArray))completion{
NSLog(#"REGISTER ACCOUNT CALLED!");
NSString *appendUrl = [NSString stringWithFormat:#"Account/Register"];
NSURL *aUrl = [NSURL URLWithString:[NSString stringWithFormat:#"%#""%#", #"xxx", appendUrl]];
NSLog(#"URL: %#",aUrl);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:aUrl
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30.0];
[request setHTTPMethod:#"POST"];
[request addValue:#"application/json, text/plain, */*" forHTTPHeaderField:#"Accept"];
[request addValue:#"application/json;charset=UTF-8" forHTTPHeaderField:#"Content-Type"];
//build an info object and convert to json
NSDictionary* info = [NSDictionary dictionaryWithObjectsAndKeys:
email,
#"Email",
password,
#"Password",
confirmPassword,
#"ConfirmPassword",
nil];
//convert object to data
NSError *error;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:info
options:NSJSONWritingPrettyPrinted error:&error];
NSString *strData = [[NSString alloc]initWithData:jsonData encoding:NSUTF8StringEncoding];
NSLog(#"%#",strData);
[request setHTTPBody:[strData dataUsingEncoding:NSUTF8StringEncoding]];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// NSLog(#"REGISTER PUSH NOTIFICATIONS RESPONSE: %#", response);
// NSLog(#"REGISTER PUSH NOTIFICATIONS ERROR: %#", error);
// NSLog(#"REGISTER PUSH NOTIFICATIONS DATA: %#", data);
NSData *_data = data;
NSMutableString *_string = [NSMutableString stringWithString:#""];
for (int i = 0; i < _data.length; i++) {
unsigned char _byte;
[_data getBytes:&_byte range:NSMakeRange(i, 1)];
if (_byte >= 32 && _byte < 127) {
[_string appendFormat:#"%c", _byte];
} else {
[_string appendFormat:#"[%d]", _byte];
}
}
NSLog(#"REGISTER ACCOUNT RESPONSE: %#", _string);
if(_string) {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
completion((NSMutableArray*)_string);
}
}];
}
Response object:
This line is all wrong:
completion((NSMutableArray*)_string);
_string is an instance of NSMutableString, and here you're telling the compiler to trust you that it's actually an instance of NSMutableArray. This is obviously a lie and when you try to use it as an array you get an exception.
Note that the JSON could be an array or a dictionary, and in your example it is actually a dictionary, so you need to check this and work out how to return an array - or change the completion block. You need to explicitly deal with this container type variation somewhere...
You don't need to do the funky data to string processing you have, you can just use NSJSONSerialization to go straight from data to JSON object (dictionary or array), and it'll return an error if there is some issue with the JSON.
Hmmm, looking at that data processing again indicates some reformatting of the data, which is a bit weird but ok. After you've done that and got your string you should turn it back into data and then use NSJSONSerialization.
I have an implementation of a PUT request in Objective C. The request is successful when executed externally from iOS / Objective C using Postman but returns an error (status code 500) when called within Objective C. As far as I can see, the implementation mirrors the way the call is set up in Postman. Here is the call I am trying to mirror using Objective C:
And here is my implementation in Objective C:
- (void)callUnregisterForPushNotifications:(NSString *)accessToken
pushToken:(NSString *)pushToken
completionBlock:(void (^)(NSMutableArray *resultsArray))completion{
NSLog(#"UNREGISTER FOR PUSH CALLED!");
NSLog(#"PUSH TOKEN %#", pushToken);
NSString *appendUrl = #"alerts/unregisterpush/";
NSLog(#"APPEND URL %#",appendUrl);
NSURL *unregisterUrl = [NSURL URLWithString:[NSString stringWithFormat:#"%#%#", BaseURLString, appendUrl]];
NSLog(#"UNREGISTER URL: %#",unregisterUrl);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:unregisterUrl
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30.0];
[request setHTTPMethod:#"PUT"];
NSString *appendToken = [NSString stringWithFormat:#"Bearer %#", accessToken];
NSLog(#"TOKEN: %#",appendToken);
[request addValue:appendToken forHTTPHeaderField:#"Authorization"];
NSString *postString = [NSString stringWithFormat:#"Guid=%#",pushToken];
NSLog(#"POST STRING: %#",postString);
[request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
NSLog(#"REQUEST %#",request);
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog(#"UNREGISTER PUSH NOTIFICATIONS RESPONSE: %#", response);
NSLog(#"UNREGISTER PUSH NOTIFICATIONS ERROR: %#", error);
NSLog(#"UNREGISTER PUSH NOTIFICATIONS DATA: %#", data);
NSData *_data = data;// ... whatever
NSMutableString *_string = [NSMutableString stringWithString:#""];
for (int i = 0; i < _data.length; i++) {
unsigned char _byte;
[_data getBytes:&_byte range:NSMakeRange(i, 1)];
if (_byte >= 32 && _byte < 127) {
[_string appendFormat:#"%c", _byte];
} else {
[_string appendFormat:#"[%d]", _byte];
}
}
NSLog(#"UNREGISTER PUSH RESPONSE: %#", _string);
id obj= [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!obj) {
//NSLog(#"REGISTER PUSH NOTIFICATIONS ERROR: %#", error);
} else {
//NSLog(#"REGISTER PUSH NOTIFICATIONS DATA: %#", obj);
if(completion) {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
completion((NSMutableArray*)obj);
}
//self.accessToken = [obj valueForKey:#"access_token"];
//NSLog(#"ACCESS TOKEN: %#",self.accessToken);
}
}];
}
Any input / help would be greatly appreciated, thanks in advance!
This line:
NSString *postString = [NSString stringWithFormat:#"Guid=%#",pushToken];
Doesn't match what you are showing in Postman.
Missing { } around the object
Missing " " around the field and value
The Accept and Content-type header are missing
I'm developing an app with a login page. When the app is launched, the login screen is shown, and you cannot access the app until you are connected. To connect to the app, you enter your username and your password. When you press the "connect" button, json data containing the username and password is sent to a web service, which check if the credentials exists. If they exists, the server send a json file containing "exists":"true"
The problem is that the code checking this Json file is in completionHandler of my NSURLSession, and the method return "NO" before the Json data is checked, so I can not connect to my app. As it's hard to explain, here is my code:
GSBconnexion.m:
#import "GSBconnexion.h"
#implementation GSBconnexion
-(bool)logConnexionWithUserName:(NSString *)username
password:(NSString *)password{
__block BOOL allowConnexion;
NSDictionary *connexion = #{
#"username": username,
#"password": password,
#"target": #"app"
};
NSError *error;
NSData *jsonLogData = [NSJSONSerialization dataWithJSONObject:connexion options:NSJSONWritingPrettyPrinted
error:&error];
if (! jsonLogData) {
NSLog(#"Got an error: %#", error);
}
NSData *logData = jsonLogData;
NSString *testString = [[NSString alloc] initWithData:logData encoding:NSUTF8StringEncoding];
NSString *logLength = [NSString stringWithFormat:#"%lu", (unsigned long)[testString length]];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:[NSURL URLWithString:#"http://192.168.5.133:1337/login"]];
[request setHTTPMethod:#"POST"];
[request setValue:logLength forHTTPHeaderField:#"Content-lenght"];
[request setValue:#"application/json" forHTTPHeaderField:#"Accept"];
[request setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:logData];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
NSDictionary *serverResponse = [NSJSONSerialization JSONObjectWithData:data options:
NSJSONReadingMutableContainers error:&error];
int canIConnect = [serverResponse[#"exist"] intValue];
NSLog(#"%d",canIConnect);
if (canIConnect == 1) {
NSLog(#"OKKK");
allowConnexion = YES;
NSString *sessionID = [[NSString alloc]initWithString:serverResponse[#"_id"]];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:sessionID forKey:#"SessionID"];
[userDefaults synchronize];
NSLog(#"ID Session:%#",[userDefaults objectForKey:#"sessionID"]);
}
else {
allowConnexion=NO;
}
}] resume];
NSLog(#"JSON envoyé: \n\n%#",testString);
return allowConnexion;
}
#end
GSBLoginController:
- (IBAction)connect:(id)sender {
connectButton.hidden = YES;
loading.hidden = NO;
UIViewController* homePage = [self.storyboard instantiateViewControllerWithIdentifier:#"homePage"];
GSBconnexion *login = [[GSBconnexion alloc]init];
NSString *username = [[NSString alloc]initWithFormat:#"%#",usernameTextField.text];
NSString *password = [[NSString alloc]initWithFormat:#"%#",pwdTextField.text];
BOOL authorized = [login logConnexionWithUserName:username password:password];
if (authorized) {
[self presentViewController:homePage animated:YES completion:nil];
}
else {
connectButton.hidden = NO;
loading.hidden=YES;
usernameTextField.text=#"";
pwdTextField.text=#"";
errorLabel.text = #"Connexion impossible, merci de réessayer.\nSi le problème persiste, veuillez contacter un administrateur.";
}
NSLog(authorized ? #"Yes" : #"No");
}
I hope you understood me, thanks for your help!
Simon
The problem is that you're expecting a return value from a method that is executing asynchronously. So basically return allowConnexion is happening immediately even though the dataTask is still ongoing in the background. Thus, you're relying on an incorrect value. Basically what you want to do is copy what is happening in the dataTask w/ a completion handler.
So you could say something like typedef void (^CompletionBlock) (BOOL isFinished);
Then change your login method to include the completion block as its last argument and return nothing:
-(void)logConnexionWithUserName:(NSString *)username
password:(NSString *)password
withCompletion:(CompletionBlock)completionBlock
Then inside of the dataTask's completionHandler call the completionBlock passing in the value of allowConnexion.
Finally once you've done all that in your login view controller you'll implement this new method, and inside of the completion block you can update your view accordingly. Its going to look something like this:
- (void)thingWithCompletion:(CompletionBlock)completionBlock
{
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(YES);
});
}
- (void)viewDidLoad {
[super viewDidLoad];
[self thingWithCompletion:^(BOOL isFinished) {
//update UI
}];
}
Be advised that since you're on a background thread and going to update UI on completion you're going to want to dispatch to the main queue as well. That is why the call to completionBlock(YES); is wrapped in the dispatch_async call.
I have a NSURLConnection (two of them), and they're running in the wrong order.
Here's my method:
- (void)loginToMistarWithPin:(NSString *)pin password:(NSString *)password {
NSURL *url = [NSURL URLWithString:#"https://mistar.oakland.k12.mi.us/novi/StudentPortal/Home/Login"];
//Create and send request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"POST"];
NSString *postString = [NSString stringWithFormat:#"Pin=%#&Password=%#",
[self percentEscapeString:pin],
[self percentEscapeString:password]];
NSData * postBody = [postString dataUsingEncoding:NSUTF8StringEncoding];
[request setHTTPBody:postBody];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
// do whatever with the data...and errors
if ([data length] > 0 && error == nil) {
NSError *parseError;
NSDictionary *responseJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
if (responseJSON) {
// the response was JSON and we successfully decoded it
NSLog(#"Response was = %#", responseJSON);
} else {
// the response was not JSON, so let's see what it was so we can diagnose the issue
NSString *loggedInPage = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Response was not JSON (from login), it was = %#", loggedInPage);
}
}
else {
NSLog(#"error: %#", error);
}
}];
//Now redirect to assignments page
NSURL *homeURL = [NSURL URLWithString:#"https://mistar.oakland.k12.mi.us/novi/StudentPortal/Home/PortalMainPage"];
NSMutableURLRequest *requestHome = [[NSMutableURLRequest alloc] initWithURL:homeURL];
[request setHTTPMethod:#"POST"];
[NSURLConnection sendAsynchronousRequest:requestHome queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *homeResponse, NSData *homeData, NSError *homeError)
{
// do whatever with the data...and errors
if ([homeData length] > 0 && homeError == nil) {
NSError *parseError;
NSDictionary *responseJSON = [NSJSONSerialization JSONObjectWithData:homeData options:0 error:&parseError];
if (responseJSON) {
// the response was JSON and we successfully decoded it
NSLog(#"Response was = %#", responseJSON);
} else {
// the response was not JSON, so let's see what it was so we can diagnose the issue
NSString *homePage = [[NSString alloc] initWithData:homeData encoding:NSUTF8StringEncoding];
NSLog(#"Response was not JSON (from home), it was = %#", homePage);
}
}
else {
NSLog(#"error: %#", homeError);
}
}];
}
- (NSString *)percentEscapeString:(NSString *)string
{
NSString *result = CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(CFStringRef)string,
(CFStringRef)#" ",
(CFStringRef)#":/?#!$&'()*+,;=",
kCFStringEncodingUTF8));
return [result stringByReplacingOccurrencesOfString:#" " withString:#"+"];
}
So, it's two NSURLConnection's that are added to the [NSOperationQueue mainQueue]. What my output is showing me is that the second NSURLConnection is running before the first one. So it tries to go to the page where I download data before I'm logged in, so it (obviously) returns a "You're not logged in" error.
How do I schedule them one after another?
The issue, as I suspect you have realized, is that you're doing asynchronous network requests (which is good; you don't want to block the main queue), so there's no assurance of the order they'll finish.
The quickest and easiest answer is to simply put the call to the second request inside the completion block of the first one, not after it. You don't want to be making that second one unless the first one succeeded anyway.
To keep your code from getting unwieldy, separate the login from the request for main page. And you can use the completion block pattern which is common with asynchronous methods. You add a parameter to loginToMistarWithPin that specifies what it should do when the request finishes. You might have one completion block handler for success, and one for failure:
- (void)loginToMistarWithPin:(NSString *)pin password:(NSString *)password success:(void (^)(void))successHandler failure:(void (^)(void))failureHandler {
NSURL *url = [NSURL URLWithString:#"https://mistar.oakland.k12.mi.us/novi/StudentPortal/Home/Login"];
//Create and send request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"POST"];
NSString *postString = [NSString stringWithFormat:#"Pin=%#&Password=%#",
[self percentEscapeString:pin],
[self percentEscapeString:password]];
NSData * postBody = [postString dataUsingEncoding:NSUTF8StringEncoding];
[request setHTTPBody:postBody];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
// do whatever with the data...and errors
if ([data length] > 0 && error == nil) {
NSError *parseError;
NSDictionary *responseJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
if (responseJSON) {
// the response was JSON and we successfully decoded it
NSLog(#"Response was = %#", responseJSON);
// assuming you validated that everything was successful, call the success block
if (successHandler)
successHandler();
} else {
// the response was not JSON, so let's see what it was so we can diagnose the issue
NSString *loggedInPage = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Response was not JSON (from login), it was = %#", loggedInPage);
if (failureHandler)
failureHandler();
}
}
else {
NSLog(#"error: %#", error);
if (failureHandler)
failureHandler();
}
}];
}
- (void)requestMainPage {
//Now redirect to assignments page
NSURL *homeURL = [NSURL URLWithString:#"https://mistar.oakland.k12.mi.us/novi/StudentPortal/Home/PortalMainPage"];
NSMutableURLRequest *requestHome = [[NSMutableURLRequest alloc] initWithURL:homeURL];
[requestHome setHTTPMethod:#"GET"]; // this looks like GET request, not POST
[NSURLConnection sendAsynchronousRequest:requestHome queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *homeResponse, NSData *homeData, NSError *homeError)
{
// do whatever with the data...and errors
if ([homeData length] > 0 && homeError == nil) {
NSError *parseError;
NSDictionary *responseJSON = [NSJSONSerialization JSONObjectWithData:homeData options:0 error:&parseError];
if (responseJSON) {
// the response was JSON and we successfully decoded it
NSLog(#"Response was = %#", responseJSON);
} else {
// the response was not JSON, so let's see what it was so we can diagnose the issue
NSString *homePage = [[NSString alloc] initWithData:homeData encoding:NSUTF8StringEncoding];
NSLog(#"Response was not JSON (from home), it was = %#", homePage);
}
}
else {
NSLog(#"error: %#", homeError);
}
}];
}
Then, when you want to login, you can do something like:
[self loginToMistarWithPin:#"1234" password:#"pass" success:^{
[self requestMainPage];
} failure:^{
NSLog(#"login failed");
}];
Now, change those successHandler and failureHandler block parameters to include whatever data you need to pass back, but hopefully it illustrates the idea. Keep your methods short and tight, and use completion block parameters to specify what an asynchronous method should do when it's done.
Can you check the below link. It is about forcing one operation to wait for another.
NSOperation - Forcing an operation to wait others dynamically
Hope this helps.
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.