Search code examples
asp.netiosuiwebviewajaxcontroltoolkit

UIWebKit Insufficiencies


Like plenty of other people before me, I have some web content that I want to display in my app. My site is an ASP.NET-based site developed in Microsoft Visual Web Developer, and it uses Ajax Toolkit among other nifty addons. I can open up the site in my iPhone's mobile Safari browser, and I've even thrown together some mobile-friendly pages.

I (or more accurately, my boss) want an iPhone app which stores credentials used to access my site and sends them automatically upon opening the app. Here's the problem:

My site doesn't load in a UIWebView. It doesn't run into any of the delegate methods either (finish loading, failed to load, etc). It simply starts up the connection request and sits there. The site does work in Safari on the same device I'm using for testing, so I figure the problem has something to do with UIWebKit not having some tools built in that Safari does.

What (specifically) is causing my site not to load in UIWebKit, when it is loading correctly in Safari?

I will post the methods I have set up to catch any signs of life from my call to

[myWebView loadRequest:myRequest];

None of the following methods are being called when I run my app and reach the loadRequest call above.

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    NSLog(@"WillSendForAuthenticationChallenge");
}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
    NSLog(@"Webview failed to load.");
    if (error.code == NSURLErrorCancelled) return; // this is Error -999, called when the user forcibly ends the connection by starting a new connection.
    UIAlertView *failure = [[UIAlertView alloc] initWithTitle:@"Failed" message:error.localizedDescription delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
    [failure show];
    [spinner stopAnimating];
}

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSLog(@"Loading request: %@",request.URL);
    return YES;
}

- (void)webViewDidFinishLoad:(UIWebView *)webview
{
    NSLog(@"Webview finished loading.");
    [spinner stopAnimating];
}

- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
    NSLog(@"Can Authenticate: %@",[protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]);
    return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    NSLog(@"Received an authentication challenge.");
    [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}

Solution

  • Well, here's how I solved it. The UIWebView never calls any of the NSURLConnection methods, and it doesn't have any built-in security methods, so when the web view is about to load a page, I first send an NSURLConnection to the same address, which can then handle the authentication through methods like didReceiveAuthenticationChallenge. The variable "_sessionChecked" is a simple BOOL which keep track of whether or not the authenticating NSURLConnection has already been sent for this page. Once it has, I can simply load the page into the web view normally.

    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
    {
        NSLog(@"Loading request: %@",request.URL);
        if (_sessionChecked) {
            NSLog(@"Session already checked.");
            return YES;
        }
    
        NSLog(@"Will check session.");
    
        NSMutableURLRequest *newReq = request.mutableCopy;
    
        [newReq setValue:USRAGNT forHTTPHeaderField:@"User-Agent"];
    
        NSURLConnection *conn = [NSURLConnection connectionWithRequest:newReq delegate:self];
        if (conn == nil) {
            NSLog(@"Cannot create connection.");
        }
        return NO;
    }
    

    The didReceiveResponse method is called when the NSURLConnection gets a response from the server. This response contains two important things: whether or not the connection was successfully authenticated with the credentials supplied by the NSURLRequest, and what url to be directed to if it was. Upon successful authentication, I send the new url to the web view to be loaded.

    - (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
    {
        NSLog(@"connection:didReceiveResponse");
    
        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            _sessionChecked = YES;
    
            NSString *newUrl = @"";
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
            int status = [httpResponse statusCode];
    
            if (status == 401 || status == 403) {
                NSLog(@"Not authenticated. http status code: %d", status);
                newUrl = @"";
                [myWebView stopLoading];
                [spinner stopAnimating];
                UIAlertView *failed = [[UIAlertView alloc] initWithTitle:@"Authentication Failed" message:@"You do not have any credentials stored, or your stored credentials are not valid." delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
                [failed show];
                _sessionChecked = NO;
            }
            else {
                NSLog(@"Authenticated. http status code: %d", status);
                newUrl = [NSString stringWithFormat:@"%@", response.URL];
            }
    
            // cancel the connection. we got what we want from the response,
            // no need to download the response data.
            [connection cancel];
    
            // start loading the new request in webView
            NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:newUrl]];
            [myWebView loadRequest:req];
        }
    }
    

    Many thanks to Ardal Ahmet for this how-to.