Slow view switch using UIViewController - ios

I'm working on my first app that integrates with a web service. Right now I have two views with their respective view controllers. I have a login view, which is where users will login to the app and I will verify and store there login credentials and I have a main view which shows all the users info from the web service. My two views work correctly individually however after verifying the credentials of the user on my login view I want to switch views to my login view. To do this I'm using UIViewController. Here is my code for when the login button is pressed:
-(IBAction)logIn:(id)sender{
//Show network activity is happening
UIApplication *application = [UIApplication sharedApplication];
application.networkActivityIndicatorVisible = YES;
//Validate credentials
[_loginNetworkingContorller checkCredentialsWithUsername:self.username.text withPassword:self.password.text completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
//if we get back a successful status code save username and password in keychain.
NSLog(#"SUCESS");
NSDictionary *credentials = #{self.username.text: self.password.text};
[KeychainUserPass save:#"INSERT APP NAME HERE" data:credentials];
NSLog(#"go to new page");
//print response from webservice for debugging purposes
NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonObject);
//switch to new view controller
UIViewController *mainController = [[RDMainViewController alloc] initWithNibName:#"RDMainViewController" bundle:nil];
[self.navigationController pushViewController:mainController animated:NO];
}
else{
//Error case, handle it.
NSLog(#"ERROR");
}
}
else{
//Error case, handle it.
NSLog(#"ERROR");
}
}];
}
And here checkCredentialsWithUsername method:
-(void)checkCredentialsWithUsername:(NSString *)username withPassword:(NSString *)password completionHandler:(void (^)(NSData *data,NSURLResponse *response, NSError *error))myCompletion
{
//Create request URL
NSString *requestString = #"WEB_SERVICE_URL";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
//Store password and user name for authentication
NSData *userPasswordData = [[NSString stringWithFormat:#"%#:%#", username, password] dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:#"Basic %#", base64EncodedCredential];
//Create an NSURL session
NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.HTTPAdditionalHeaders=#{#"Authorization":authString};
self.session=[NSURLSession sessionWithConfiguration:sessionConfig];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
myCompletion(data, response, error);
}];
[dataTask resume];
}
My issue is that it's taking an incredibly long time to switch views. Like sometimes over a minute. At first I thought it was the network connection but then I printed the data I was receiving from the web service and it was appearing very quickly. Even after I printed the data it was still taking a very long time for views to switch. I'm not really sure why but I think it has to do with the way I'm doing my blocks. Any idea why it's taking me such a long time to switch views?
Any help would be greatly appreciated.

#Danyun was right. I needed to do the following:
if (httpResp.statusCode == 200) {
//if we get back a successful status code save username and password in keychain.
NSLog(#"SUCESS");
NSDictionary *credentials = #{self.username.text: self.password.text};
[KeychainUserPass save:#"INSERT APP NAME HERE" data:credentials];
NSLog(#"go to new page");
//print response from webservice for debugging purposes
NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonObject);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//test to make sure we are on the main Queue for UI update
//switch to the new view controller
UIViewController *mainController = [[RDMainViewController alloc] initWithNibName:#"RDMainViewController" bundle:nil];
[self.navigationController pushViewController:mainController animated:NO];
}];
}

Related

login and completion handler

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.

How to use an activity indicator for web service login

I just created an iOS app which uses web service and my problem here is when I tap log in the view controller is idle not displaying the activity indicator but when the response and data from web service is fetched and the next view controller is loaded; the activity indicator quickly appears. I want the activity indicator visible at the time when I tap the log in button
Code sample:
NSString *username = TextUsername.text;
NSString *password = Textpassword.text;
NSString *strLogInUrl=[NSString stringWithFormat:#""];
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
NSLog(#"Url %#",strLogInUrl);
NSURL *URLGet = [NSURL URLWithString:strLogInUrl];
NSData *data = [NSData dataWithContentsOfURL:URLGet];
NSError* error;
DicForAllData = [NSJSONSerialization JSONObjectWithData: data options:kNilOptions error:&error];
As others have pointed out, to let the UI update, you want to perform this asynchronously. Before I drag you through the weeds on some details, let's consider a high-level abstraction of the code in your question:
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
NSData *data = [NSData dataWithContentsOfURL:URLGet];
// do something with data
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
Instead of that synchronous request, you really want to issue an asynchronous request like so:
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
NSMutableURLRequest *request = ...;
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// handle the data or connectionError here, inside this block, and remember to turn off activity indicator
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}];
The thing is, your code snippet is not creating the request properly. You've over-simplified the process. Your process should really also consider:
If you're supplying credentials, that's invariably a POST request, not a simple GET that dataWithContentsOfURL performs.
You really should probably construct a proper application/x-www-form-urlencoded request.
You have to percent escape the data being passed to the server (without that, if the password happened to contain certain reserved characters, such as + or &, the login would fail).
You really should be handling errors.
Pulling all of that together, it looks more like:
// tell the user we're going to do this request for them
// maybe show a `UIActivityIndicatorView`, too
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
// construct the body of the request; replace the keys `username` and `password` with whatever your web service requires
NSString *username = textUsername.text;
NSString *password = textPassword.text;
NSString *parameterString = [NSString stringWithFormat:#"username=%#&password=%#", [self percentEscapeString:username], [self percentEscapeString:password]];
NSData *httpBody = [parameterString dataUsingEncoding:NSUTF8StringEncoding];
// now create the request itself
NSString *strLogInUrl = #"http://example.com";
NSURL *URLGet = [NSURL URLWithString:strLogInUrl];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URLGet];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:httpBody];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[request setValue:#"application/json" forHTTPHeaderField:#"Accept"];
// now send the request
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (!data) {
NSLog(#"connectionError: %#", connectionError);
} else {
NSError* error;
dicForAllData = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
// do what you want with the data/error here, e.g. `[self.tableView reloadData]` or whatever
}
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}];
And that assumes that you have a percent escaping routine, something like:
- (NSString *)percentEscapeString:(NSString *)string
{
NSString *result = CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(CFStringRef)string,
(CFStringRef)#" ",
(CFStringRef)#":/?#!$&'()*+,;=",
kCFStringEncodingUTF8));
return [result stringByReplacingOccurrencesOfString:#" " withString:#"+"];
}
Earlier, UnicoRahul inquired whether you were using AFNetworking. It's worth considering as it greatly simplifies the code you have to write:
NSDictionary *params = #{#"username": username, #"password": password};
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager POST:#"http://example.com" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
dicForAllData = responseObject;
// do what you want with the data here, e.g. `[self.tableView reloadData]` or whatever
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%#", error);
}];
It sounds like you're using synchronous code to log in. If you do something like this:
display activity indicator
log in (synchronously)
hide activity indicator
Then the activity indicator never gets a chance to display. (UI changes don't get rendered to the screen until after your code returns and the app visits the event loop.)
You should either change your login code to work asynchronously (best option) or rewrite your code to visit the event loop between displaying the activity indicator and logging in. Pseudocode might look like this:
display activity indicator
[self performSelector: #selector(login) withObject: nil afterDelay: 0];
And your login method pseudocode:
- (void) login;
{
log in (synchronously)
hide activity indicator
}
(performSelector:withObject:afterDelay: does not execute the code from the selector right away. Instead, it queues up a call to your selector for the next time the app visits the event loop. Your code gets called after the system has a chance to update the screen {specifically, after it gets a chance to display your activity indicator}.)
Once you tap on login button, before calling web service call activity indicator and after a second call web service so, the activity indicator will be shown/displayed. Once you get the response first stop the activity indicator and then push to next view controller.
Remember before pushing to next screen make the keyboard dismiss. it's a good practice.

UIKeyboardTaskQueue threading issue

I'm fairly new to iOS development and I've been stuck on this bug for a while. I'm making a simple app the uses a web service. Right now I currently have two view controllers. A login view controller (with its NIB file) and a main view controller (with its NIB file). When I created the app I chose an empty application so I don't have a storyboard. Instead I'm using UINavigationController. When I run my code I get the following error after entering my username and password and pressing submit in the login view:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[UIKeyboardTaskQueue waitUntilAllTasksAreFinished] may only be called from the main thread.'
This is the code I have for my submit button:
-(IBAction)logIn:(id)sender{
UIApplication *application = [UIApplication sharedApplication];
application.networkActivityIndicatorVisible = YES;
[_loginNetworkingContorller checkCredentialsWithUsername:self.username.text withPassword:self.password.text completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
NSLog(#"SUCESS");
NSDictionary *credentials = #{self.username.text: self.password.text};
[KeychainUserPass save:#"MY APP" data:credentials];
UIViewController *mainView = [[RDMainViewController alloc] initWithNibName:#"RDMainViewController" bundle:nil];
[self.navigationController pushViewController:mainView animated:YES];
}
else{
NSLog(#"ERROR");
}
}
else{
NSLog(#"ERROR");
}
}];
}
And here is the code for the following function
[_loginNetworkingContorller checkCredentialsWithUsername:self.username.text withPassword:self.password.text completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
-(void)checkCredentialsWithUsername:(NSString *)username withPassword:(NSString *)password completionHandler:(void (^)(NSData *data,NSURLResponse *response, NSError *error))myCompletion
{
NSString *requestString = #"SOME WEBSITE";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSData *userPasswordData = [[NSString stringWithFormat:#"%#:%#", username, password] dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:#"Basic %#", base64EncodedCredential];
NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.HTTPAdditionalHeaders=#{#"Authorization":authString};
self.session=[NSURLSession sessionWithConfiguration:sessionConfig];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
myCompletion(data, response, error);
}];
[dataTask resume];
}
I'm a stuck right now and am not really sure what the issue is especially since I don't do anything with the keyboard. I have a feeling there is an issue with my blocks but I'm not exactly sure what the issue is. Any help would be greatly appreciated.
Hey I had this same issue after I received my response from my web call. I was able to solve it be doing the following:
//do something with response
dispatch_async(dispatch_get_main_queue()) { () -> Void in
// continue with program by calling next step on main thread
}
I think if you push to the next view controller iOS attempts to do it on not the main thread, causing the error, but I'm not 100% is that is accurate

Block not completing in iOS

I'm relatively new to iOS development but I'm working on an application to get a better understanding of development. I'm working with a web service and want to check the credentials a user enters. To do this I am making a simple get request with their credentials and then checking the http status for 200. Here is my code below:
-(BOOL)checkCredentials:(NSString *)username withPassword:(NSString *)password{
NSString *requestString = #"SOME URL";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSData *userPasswordData = [[NSString stringWithFormat:#"%#:%#", username, password] dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:#"Basic %#", base64EncodedCredential];
NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.HTTPAdditionalHeaders=#{#"Authorization":authString};
self.session=[NSURLSession sessionWithConfiguration:sessionConfig];
__block BOOL success = NO;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
success = YES;
}
}
NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonObject);
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[dataTask resume];
return success;
}
I was going to use a semaphore to wait for the block to complete so I can check the status code and then return. But first it seems like my code just hangs, and I think that because I don't have a release, but that's not allowed with ARC. I'm not sure why it's hanging. Is there a better way to wait for the block to complete (without a semaphore) so I can return whether my credentials are valid?
Also is there a better way to pass the username and password so that it's not possible for someone to spoof the username and password?
Any help would be greatly appreciated.
Think simple!
Make your own completionHandler so that you won't deal with the return anymore, the caller will take the responsibility of result verification instead.
There's one thing you need to keep in mind, that if you want to modify anything related to UI (User Interface), you need to dispatch your completion block to main queue or you will get unexpected behavior, see more detail here.
Change your return type to void and add a completion block:
-(void)checkCredentials:(NSString *)username withPassword:(NSString *)password completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))myCompletion
{
NSString *requestString = #"http://google.com";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// Here you return exactly what the NSURLSessionDataTask downloaded
// and pass it to the caller as an another completion block
myCompletion(data, response, error);
}];
[dataTask resume];
}
Caller's code, I assume that self is the caller:
[self checkCredentials:#"" withPassword:#"" completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
// Result verification's here
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
NSLog(#"SUCESS");
}
}
}];
You code stops waiting for a semaphore and [dataTask resume] is never executed.
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); <=== waits here
[dataTask resume]; <=== never reached
I'd suggest not using the semaphore here. Do the work in your block instead.
As to username/password. If you worry about spoofing then SSL layer on top of HTTP is the answer.
This is a really dangerous pattern, because this call is going to block until the network request completes. If this is on the main thread, your app will stop responding and the watchdog may kill you.
That warning aside, the reason the block doesn't complete is because the network task is never started. You trap on your semaphore before you call resume, so your task never runs. I would also, personally use a dispatch_group to do the waiting.
To make it better, you would need to rewrite it asynchronously. Basically have your app continue to function, maybe disable the inputs, until the call completes, then run a block to re-enable them, or show an error:
// Assume your login button and whatever are exposed as properties here
self.loginButton.enabled = NO;
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
success = YES;
}
}
NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonObject);
// Need to be back on the main queue, the call is complete
self.loginButton.enabled = YES;
}];
[dataTask resume];
Or, just to keep it the way you have it, but resolve the immediate issue, re-order your trap so that it happens after the task resumes:
[dataTask resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); // might want to time out here instead of waiting forever
return success;

NSURLConnection wrong order

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.

Resources