views:

5722

answers:

5

Part of my app caches web pages for offline viewing. To do that, I am
saving the HTML fetched from a site and rewriting img urls to point to
a file on the local store. When I load the html into a UIWebView, it
loads the images as expected and everything's fine. I am also caching
stylesheets in this fashion.

The problem is that when I put the phone into airplane mode, loading
this cached html causes the UIWebView to display a blank screen and
pause for a while before displaying the page. I've figured out that
it's caused by non-cached URLs referenced from the original HTML doc that
the web view is trying to fetch. These other URLs include images within
the cached stylesheets, content in iframes, and javascript that opens a
connection to fetch other resources. The pause happens when the
UIWebView tries to fetch these resources, and the web page only appears after all these other fetches have timed out.

My questions is, how can I make UIWebView just display the stuff I've
cached immediately? Here are my thoughts:

  • write even more code to cache these other references. This is
    potentially a ton more code to catch all the edge cases, etc., especially having to parse the Javascript to see what it loads after the page is loaded
  • force UIWebView to time out immediately so there's no pause. I
    haven't figured out how to do this.
  • somehow get what's already loaded to display even though the external references haven't finished fetching yet
  • strip the code of all scripts, link tags and iframes to "erase" the
    external references. I've tried this one, but for some sites, the
    resultant page is severely messed up

Can anyone help me here? I've been working on this forever, and am
running out of ideas.

+1  A: 

Generate a NSURLRequest with +requestWithURL:cachePolicy:timeoutInterval:. Set the cache policy to NSURLRequestReturnCacheDataDontLoad. Load the request into the webview using -loadRequest:. Full docs here.

Rob Napier
Hi, my reply comment was too long so I stuck it in an answer below.
leftspin
+1  A: 

Thanks for the lead! It got me unstuck. Unfortunately, I seem to be running into a bunch of roadblocks.

The problem is that if I use NSURLRequestReturnCacheDataDontLoad, I think the resources that I'm loading must already be in the cache.

To put them in the cache, I tried this:

 NSURLRequest *request =  [NSURLRequest 
       requestWithURL:cachedURL 
       cachePolicy:NSURLRequestReloadIgnoringLocalCacheData 
       timeoutInterval:60.0] ;

 NSURLResponse *responseToCache = 
      [[NSURLResponse alloc]
       initWithURL:[request URL] 
       MIMEType:@"text/html" 
       expectedContentLength:[dataAtURL length] 
       textEncodingName:nil] ;

 NSCachedURLResponse *cachedResponse = 
      [[NSCachedURLResponse alloc]
       initWithResponse:responseToCache data:dataAtURL
       userInfo:nil storagePolicy:NSURLCacheStorageAllowedInMemoryOnly] ;

  // Store it
 [[NSURLCache sharedURLCache] storeCachedResponse:cachedResponse forRequest:request] ;

 [responseToCache release] ;
 [cachedResponse release] ;

 NSLog( @"*** request %@, cache=%@", request ,[[NSURLCache sharedURLCache] 
   cachedResponseForRequest:request]       ) ;

I got this code from elsewhere on the 'net, but I think that although it worked on the Mac, it isn't working on the iPhone, my target platform.

It doesn't seem to insert the item into the cache; the NSLog prints null for "cache=%@".

Then, I tried overloading NSURLCache:

- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
    {
    NSLog( @"FileFriendlyURLCache asked from request %lx of type %@ for data at URL: %@" , 
     request , [request class] , [[request URL] absoluteString] ) ;

    NSCachedURLResponse *result = nil ;

    if( [[[request URL] absoluteString] hasPrefix:@"file://"] )
     {
     NSLog( @"\tFulfilling from cache" ) ;

     NSError *error = nil ;
     NSData *dataAtURL = [NSData dataWithContentsOfURL:[request URL] 
         options:0 error:&error] ;
     if( error )
      NSLog( @"FileFriendlyURLCache encountered an error while loading %@: %@" ,
       [request URL] , error ) ;

     NSURLResponse *reponse = [[NSURLResponse alloc]
            initWithURL:[request URL] 
            MIMEType:@"text/html" 
            expectedContentLength:[dataAtURL length] 
            textEncodingName:nil] ;
     result = [[NSCachedURLResponse alloc] initWithResponse:reponse data:dataAtURL
      userInfo:nil storagePolicy:NSURLCacheStorageAllowedInMemoryOnly] ;

     #warning LEOPARD BUG MAKES IT SO I DONT AUTORELEASE result NSCachedURLResponse

     [reponse release] ;
     }
    else
     {
     NSLog( @"\tFulfilling from web" ) ;
     result = [super cachedResponseForRequest:request] ;
     }

    NSLog( @"Result = %@" , result ) ;

    return result ;
    }

In my request, I specify a cache policy of NSURLRequestReturnCacheDataDontLoad. This method seems to be called just fine, but it has strange behaviors on the iPhone. Regardless of whether or not I return an NSCachedURLResponse instance, the UIWebView still returns an error:

Error Domain=NSURLErrorDomain Code=-1008 UserInfo=0x45722a0 "resource unavailable"

It's as if the UIWebView ignores the fact that it's getting something other than nil back and failing anyway. I then got suspicious and wondered if the entire URL loading system simply just ignored everything coming from -cachedResponseForRequest, so I created a subclass of NSCachedURLResponse that looks like this:

@implementation DebugCachedURLResponse

- (NSData *)data
    {
    NSLog( @"**** DebugCachedURLResponse data accessed." ) ;
    return [super data] ;
    }

- (NSURLResponse *)response
    {
    NSLog( @"**** DebugCachedURLResponse response accessed." ) ;
    return [super response] ;
    }

- (NSURLCacheStoragePolicy)storagePolicy
    {
    NSLog( @"**** DebugCachedURLResponse storagePolicy accessed." ) ;
    return [super storagePolicy] ;
    }

- (NSDictionary *)userInfo
    {
    NSLog( @"**** DebugCachedURLResponse userInfo accessed." ) ;
    return [super userInfo] ;
    }
@end

I then modified -cachedResponseForRequest to use this class instead:

 result = [[DebugCachedURLResponse alloc] initWithResponse:reponse data:dataAtURL
  userInfo:nil storagePolicy:NSURLCacheStorageAllowedInMemoryOnly] ;

I set the request to have a NSURLRequestUseProtocolCachePolicy cache policy and ran the program. Although I can see my version of -cachedResponseForRequest being called and returning DebugCachedURLResponses for all file URLs, none of my debugging is being called, telling me that my DebugCachedURLResponse instances are being completely ignored!

So, here again, I'm stuck. I don't suppose you have any other ideas?

Many thanks for your first reply.

leftspin
Hi leftspin - I've spent a lot of time going through similar trials and tribulations. In some previous code, I believe it actually accepted the cached responses, but now I'm having the same results as you (it is getting my cached response but ignoring it). Any luck since you posted this?
Tyler
Nope, sorry, no luck.
leftspin
I've just posted an answer to a similar question here : http://stackoverflow.com/questions/1343515/how-to-save-the-content-in-uiwebview-for-faster-loading-on-next-launch/2468722#2468722 My approach was the same : subclass the NSURLCache, but I never call any [super and fully rely on my own mechanism : I'm handling the request on my own. This works ok on my side even if I think that it does not fully respect the cache approach of the cache disk / memory : everything is forced to the FS. But I couldn't think any more elegant solution here :/
yonel
+3  A: 

I've been looking at this myself for the past while, not actually implementing anything yet, just Googling. It seems overall that it's very hard / impossible to do this on the iPhone using DontLoad. Even in March, there were reports that it works in Simulator, but not on device: http://discussions.apple.com/message.jspa?messageID=9245292

I'm just guessing, but based on even other StackOverflow questions (e.g. this one) I'm thinking perhaps the on-device caching is somehow more limited than both its Mac and Safari counterparts.

Others on the Apple forums report experiences with rude engineers when asking about UIWebView and NSURLCache, where in bug reports the engineers say it should work, but actual developers say it doesn't. http://discussions.apple.com/thread.jspa?threadID=1588010 (Jul 29-Aug 20)

Some solutions might be found here. Considering [UIImage imageWithData:...], David describes some code for "an implementation of an asynchronous, caching image downloader for iPhone."

And now I've discovered this Aug 10th email on the cocoa-dev mailing list:

On Aug 10, 2009, at 1:38 PM, Mike Manzano wrote:

Has anyone been able to successfully get the URL loading system on iPhone to pay attention to custom versions of NSURLCache? It seems to me that it's being ignored. More info here:

[a link to this page; removed.]

(See the second "answer" on that page).

Just need some sort of clue if I'm doing something wrong or if my code's just being ignored.

Yes, I've had it work - it just requires making your own cache instance and explicitly setting the shared cache to that new instance.

But it absolutely did work - once implemented, my "set the table view cell's image to an image from the net" works nicely (and without it scrolling was painful at best).

Glenn Andreas gandreas@xxxxxxxxxxxx http://www.gandreas.com/ wicked fun! Mad, Bad, and Dangerous to Know

The above email can be found at http://www.cocoabuilder.com/archive/message/cocoa/2009/8/10/242484 (with no further replies)

Further proof that NSURLCache can work is found in iCab Blog: URL filtering for UIWebView on the iPhone (Aug 18, 2009)

Anyone here probably should also look to Apple's URLCache sample app as it may be relevant to their work: https://developer.apple.com/iphone/library/samplecode/URLCache/index.html Except having just looked at it now, it says "This application does not use a NSURLCache disk or memory cache, so our cache policy is to satisfy the request by loading the data from its source." and uses cachePolicy:NSURLRequestReloadIgnoringLocalCacheData. So this is less relevant than I thought, and why this person was calling NSURLCache a joke.

So it seems NSURLCache is not as tricky or impossible as I started this post by saying. Gotta love finding a way that works while researching reasons it won't work. I still can't believe there are only 25 Google search results for "iphone" and "NSURLRequestReturnCacheDataDontLoad"... This answer has just about every one of em. And I'm glad I wrote it so I can refer to it later ;-)

Louis St-Amour
Thanks for taking time to summarize this.
jm
+1  A: 

Hey guys, I think I found the problem. It looks like NSURLRequestReturnCacheDataElseLoad and NSURLRequestReturnCacheDataDontLoad is broken on the iPhone. When a NSCachedURLResponse is returned from the cache and it contains HTTP headers indicating that the content has expired (e.g. Expires, Cache-Control etc), the cached response is ignored and the request is made to the original source.

The solution is as follows:

  1. Implement your own subclass of NSHTTPURLResponse which allows you to modify the allHeaderFields dictionary.
  2. Implement your own NSURLCache, override cachedResponseForRequest: and return a new NSCachedURLResponse containing an instance of your NSHTTPURLResponse subclass with the relevant "expiry" HTTP headers stripped.
NP
Step 2 not so clear. Could you post some code?
jm
A: 

leftspin, is there any chance of sharing your code that caches images from the UIWebView into the disk? I have the same problem here...

WrongEra