Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically loading javascript files in UIWebView with local cache not working

I'm trying to write an application which uses a lot of java script inside a UIWebView. And some of this java script is loaded dynamically (with jquery) when it is needed.

The index.html and the jquery files are loaded as expected but not the code.js file. The code.js file is request like this (snipped of the java script code from index.html):

function require(moduleName,filePath) {
    var uri = filePath;
    jQuery.ajax({
        type: "GET",
        url: uri,           
        async: false,
        dataType: "script",
        success: function(content) {
            console.log("Receive content of "+moduleName);
        },
        error: function(XMLHttpRequest, textStatus, errorThrown) {      
            console.log("Error content of "+moduleName);
            console.log("Status: " + textStatus);
            console.log("Thrown error: " + errorThrown);
            console.log("h" + XMLHttpRequest.getAllResponseHeaders());
            console.log(XMLHttpRequest.responseText);
        }
    });
}

function start() {
    require("test", "code.js");
}

The start() function is called in the onload event of the body tag of the index.html. The code.js file only contains the following three lines of code:

alert("Loaded file syncronosly");
// HEllo
alert("second");

The output I get from this code looks like this (I have some additional js code which forwards the console.log calls to the Xcode console which I omitted):

UIWebView console: Error content of test
UIWebView console: Status: error
UIWebView console: Thrown error: 
UIWebView console: h
UIWebView console: HTTP Error (0 error).
UIWebView console: alert("Loaded file syncronosly");
// HEllo
alert("second");

I get this behavior as soon as I try to return the content of code.js in the overridden NSURLCache class. The idea in the end is to have a application which of which a part runs in a UIWebView without the need to load the content from an external web server.

This is the shortened code of the cache class (LocalSubstitutionCache):

NSString *pathString = [[request URL] absoluteString];
NSLog(@"request => %@", pathString);

NSString* fileToLoad = nil;
if ([pathString isEqualToString:@"http://example.com/index.html"]) {
   fileToLoad = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
}
else if ([pathString hasPrefix:@"http://example.com/code.js"]) {
    fileToLoad = [[NSBundle mainBundle] pathForResource:@"code" ofType:@"js"];
}
else if ([pathString isEqualToString:@"http://example.com/jquery-1.6.2.min.js"]) {
    fileToLoad = [[NSBundle mainBundle] pathForResource:@"jquery-1.6.2.min" ofType:@"js"];
}

if (!fileToLoad) {
    // No local data needed for this request let super handle it:
    return [super cachedResponseForRequest:request];
}

NSData *data = [NSData dataWithContentsOfFile:fileToLoad];

NSURLResponse *response =
    [[NSURLResponse alloc]
        initWithURL:[request URL]
        MIMEType:[self mimeTypeForPath:pathString]
        expectedContentLength:[data length]
        textEncodingName:nil];
cachedResponse =
    [[NSCachedURLResponse alloc] initWithResponse:response data:data];

(The cache code is from here: http://cocoawithlove.com/2010/09/substituting-local-data-for-remote.html)

Now in my view controller (which contains a web view) I do the following:

- (void)viewDidLoad {
    [super viewDidLoad];

    // This line loads the content with the cache enabled (and does not work):
    [_webview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com/index.html"]]];
}

Now the interesting thing is that when I replace the code in the viewDidLoad method with the following code:

- (void) viewDidLoad {
    [super viewDidLoad];

    // This two line load the content without the cache:
    NSString* fileToLoad = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
    [_webview loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:fileToLoad]]];
}

Now all is working fine and I get my two alert's displayed. So I suspect that what I'am returning in the cache is not the same as what the original request is returning when it is catches by the cache.

Here is what I already checked for:

  • Not using file urls for the working case. My original code gets the not cached code from a web server. But for the example project I'm using file urls as this makes the project simpler .
  • Content type returned. Is on both cases text/javascript
  • Specials headers set. I've checked with a http proxy what headers get set on the request which is not cached. There are no special headers set.

My biggest issue is that the jquery error callback does not return any useful error information. I just get an output of error for the textStatus argument.

You can download the example project from here: http://dl.dropbox.com/u/5426092/LocalCacheJsInjectTest.zip

Hope somebody can help me here

like image 737
Chris Avatar asked Feb 14 '12 15:02

Chris


1 Answers

After some more more work I found the solution to my problem. And answer here with what I found.

The solution is to to create a custom NSURLProtocol implementation and register this in the application as handler for the http protocol. The code looks like this (I've omitted some error handling in the code):

#import "MyHttpURLProtocol.h"

@implementation MyHttpURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    return [[[request URL] scheme] isEqualToString:@"http"]; // we handle http requests
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

- (void) startLoading {
    id<NSURLProtocolClient> client = [self client];
    NSURLRequest* request = [self request];
    NSString *pathString = [[request URL] absoluteString];

    NSString* fileToLoad = nil;
    if ([pathString isEqualToString:@"http://example.com/index.html"]) {
        fileToLoad = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
    }
    else if ([pathString hasPrefix:@"http://example.com/code.js"]) {
        fileToLoad = [[NSBundle mainBundle] pathForResource:@"code" ofType:@"js"];
    }
    else if ([pathString isEqualToString:@"http://example.com/jquery-1.6.2.min.js"]) {
        fileToLoad = [[NSBundle mainBundle] pathForResource:@"jquery-1.6.2.min" ofType:@"js"];
    }

    NSData* data = [NSData dataWithContentsOfFile:fileToLoad];

    NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:[request URL] statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:[NSDictionary dictionary]];

    [client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [client URLProtocol:self didLoadData:data];
    [client URLProtocolDidFinishLoading:self];
}

- (void) stopLoading {}

@end

This code allows now to load the index.html file be accessing the following code:

[_webview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com/index.html"]]];

And don't forget to register the custom protocol in your app delegate:

[NSURLProtocol registerClass:[TOHttpURLProtocol class]];

[NSURLProtocol registerClass:[MyHttpURLProtocol class]];

It's amazing how simple the solution can be after your found it. Here is the link to the updated example project: http://dl.dropbox.com/u/5426092/LocalCacheJsInjectTest-working.zip

And for completeness sake here is the link to the blog post which helped me in finding the solution: http://www.infinite-loop.dk/blog/2011/09/using-nsurlprotocol-for-injecting-test-data/

like image 189
Chris Avatar answered Nov 07 '22 21:11

Chris