How to save the content in UIWebView for faster loading on next launch?

20,943

Solution 1

There are a bunch of articles about the way the cache of the UIWebView works and the global feeling is that even if some mechanisms seems to work OK under MacOS X, the same approaches may have curious behavior under iPhone.


HOWEVER, I'm doing it by playing with the global cache that is accessed by any NSURLConnection, UIWebView included. And in my case, it works ;).

What you need to understand is the global flow:

  • YOU -> loadRequest on a UIWebView
  • This goes into NSURLCache to ask "is there something cached for this request?":
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request

From that, here's what I do to handle the cache on the disk, on my side, to speed up the load of a UIWebView:

  • Subclass the NSURLCache and override the get control over the -(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request selector
  • Reimplement this selector in such a way that if nothing has been written on the FS for this request (no cache), then do the request on your side and store the content on FS. Otherwise, return what has been previously cached.
  • Create an instance of your subclass and set it to the system so that it is used by your application

Now the code :

MyCache.h

@interface MyCache : NSURLCache {
}
@end

MyCache.m

@implementation MyCache

-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSLog(@"CACHE REQUEST S%@", request);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSArray* tokens = [request.URL.relativePath componentsSeparatedByString:@"/"];
    if (tokens==nil) {
        NSLog(@"ignoring cache for %@", request);
        return nil;
    }
    NSString* pathWithoutRessourceName=@"";
    for (int i=0; i<[tokens count]-1; i++) {
        pathWithoutRessourceName = [pathWithoutRessourceName stringByAppendingString:[NSString stringWithFormat:@"%@%@", [tokens objectAtIndex:i], @"/"]];
    }
    NSString* absolutePath = [NSString stringWithFormat:@"%@%@", documentsDirectory, pathWithoutRessourceName];
    NSString* absolutePathWithRessourceName = [NSString stringWithFormat:@"%@%@", documentsDirectory, request.URL.relativePath];
    NSString* ressourceName = [absolutePathWithRessourceName stringByReplacingOccurrencesOfString:absolutePath withString:@""];
    NSCachedURLResponse* cacheResponse  = nil;
    //we're only caching .png, .js, .cgz, .jgz
    if (
        [ressourceName rangeOfString:@".png"].location!=NSNotFound || 
        [ressourceName rangeOfString:@".js"].location!=NSNotFound ||
        [ressourceName rangeOfString:@".cgz"].location!=NSNotFound || 
        [ressourceName rangeOfString:@".jgz"].location!=NSNotFound) {
        NSString* storagePath = [NSString stringWithFormat:@"%@/myCache%@", documentsDirectory, request.URL.relativePath];
        //this ressource is candidate for cache.
        NSData* content;
        NSError* error = nil;
        //is it already cached ? 
        if ([[NSFileManager defaultManager] fileExistsAtPath:storagePath]) {
            //NSLog(@"CACHE FOUND for %@", request.URL.relativePath);
            content = [[NSData dataWithContentsOfFile:storagePath] retain];
            NSURLResponse* response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:@"" expectedContentLength:[content length] textEncodingName:nil];
            cacheResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:content];
        } else {
            //trick here : if no cache, populate it asynchronously and return nil
            [NSThread detachNewThreadSelector:@selector(populateCacheFor:) toTarget:self withObject:request];
        }
    } else {
        NSLog(@"ignoring cache for %@", request);
    }
    return cacheResponse;
}

-(void)populateCacheFor:(NSURLRequest*)request {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    //NSLog(@"PATH S%@", paths);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSArray* tokens = [request.URL.relativePath componentsSeparatedByString:@"/"];
    NSString* pathWithoutRessourceName=@"";
    for (int i=0; i<[tokens count]-1; i++) {
        pathWithoutRessourceName = [pathWithoutRessourceName stringByAppendingString:[NSString     stringWithFormat:@"%@%@", [tokens objectAtIndex:i], @"/"]];
    }
    NSString* absolutePath = [NSString stringWithFormat:@"%@/myCache%@", documentsDirectory, pathWithoutRessourceName];
    //NSString* absolutePathWithRessourceName = [NSString stringWithFormat:@"%@%@", documentsDirectory, request.URL.relativePath];
    //NSString* ressourceName = [absolutePathWithRessourceName stringByReplacingOccurrencesOfString:absolutePath withString:@""];
    NSString* storagePath = [NSString stringWithFormat:@"%@/myCache%@", documentsDirectory, request.URL.relativePath];
    NSData* content;
    NSError* error = nil;
    NSCachedURLResponse* cacheResponse  = nil;
    NSLog(@"NO CACHE FOUND for %@", request.URL);
    //NSLog(@"retrieving content (timeout=%f) for %@ ...", [request timeoutInterval], request.URL);
    content = [NSData dataWithContentsOfURL:request.URL options:1 error:&error];
    //NSLog(@"content retrieved for %@  / error:%@", request.URL, error);
    if (error!=nil) {
        NSLog(@"ERROR %@ info:%@", error, error.userInfo);
        NSLog(@"Cache not populated for %@", request.URL);
    } else {
        NSURLResponse* response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:@"" expectedContentLength:[content length] textEncodingName:nil];
        cacheResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:content];
        //the store is invoked automatically.
        [[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:&error];
        BOOL ok;// = [[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:&error];
        ok = [content writeToFile:storagePath atomically:YES];
        NSLog(@"Caching %@ : %@", storagePath , ok?@"OK":@"KO");
    }
    [pool release];
}
@end

And the use of it in your application:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* documentsDirectory = [paths objectAtIndex:0];
NSString* diskCachePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, @"myCache"];
NSError* error; 
[[NSFileManager defaultManager] createDirectoryAtPath:diskCachePath withIntermediateDirectories:YES attributes:nil error:&error];
MyCache* cacheMngr = [[MyCache alloc] initWithMemoryCapacity:10000 diskCapacity:100000000 diskPath:diskCachePath];
[NSURLCache setSharedURLCache:cacheMngr];

This code deserves a lot of cleanup.. but the main things should be in there. I had a lot of trouble to get this working, hope this helps.

Solution 2

I recently found this project under github : http://github.com/rs/SDURLCache The approach is quite the same as my previous answer described here How to save the content in UIWebView for faster loading on next launch? , but the code looks more polished so maybe it makes sense to give it a try.

Solution 3

If the page has AJAX already, why not store the JavaScript/HTML in the application bundle to start rather than downloading it on the first launch? Then load the page with the code Corey gave below and let the AJAX handle hitting the network for the updated parts of the page.

Solution 4

Take a look at: http://allseeing-i.com/ASIHTTPRequest/ASIWebPageRequest

Solution 5

You can save an HTML in the documents directory and load the page directly from the documents directory on launch.

To save the webview content: Reading HTML content from a UIWebView

To load:

    NSString* path = [[NSBundle mainBundle] pathForResource:@"about" ofType:@"html"];
    NSURL* url = [NSURL fileURLWithPath:path];

    NSURLRequest* request = [NSURLRequest requestWithURL:url];
    [webView loadRequest:request];
Share:
20,943
erotsppa
Author by

erotsppa

Updated on July 09, 2022

Comments

  • erotsppa
    erotsppa almost 2 years

    I know that there are some caching classes introduced in the iphone sdk recently, and there is also a TTURLRequest from three20's library that allows you to cache a request to a URL. However, because I am loading the web page in UIWebView by calling UIWebView's loadRequest, those techniques are not really applicable.

    Any ideas how I can save a web page so that on next app launch, I don't have to fetch from the web again for the full page? The page itself already have some ajax mechanism that updates parts of itself automatically.

  • Vladimir
    Vladimir almost 14 years
    Thanks. Your approach seems the only way to save web page contents with all resources without html parsing. But as I see we in fact load each resource two times: by webView itself when we returning nil responce and then we load same request in "populateCacheFor" method. Any ideas how to solve this?
  • Dave Peck
    Dave Peck almost 14 years
    The trick you used in cachedResponseForRequest:request: is clever, but loading resources twice sounds vaguely evil. :-) Is there really no way to get UIWebView to play nice with the rest of the world?
  • yonel
    yonel almost 14 years
    hum, yes, I didn't pay attention to that but you're right. I cannot remember exactly why I populated the cache asynchronously, but maybe it would make sense to make it synchronous and return the result instead of nil to the UIWebView (avoiding the request to be performed again by the web view... :/ ) I may have a look at that since I'm still in that stuff.... (unfortunatly :/ )
  • yonel
    yonel almost 14 years
    I checked that. doing it synchronously works , so the correct flow is (synchronously), check if the cache has been populated, if not, do it synchronously, and then return it. The consequence is that nil is never returned anymore.
  • Nick Baicoianu
    Nick Baicoianu about 13 years
    Unfortunately you're stuck between a rock and a hard place – load asynchronously and you're going to be fetching every resource twice. Load synchronously and you're blocking other resources (most noticeable with images) from loading while downloading an uncached resource. Either method is better than a completely uncached system.