5

I have a problem I have discovered in my app that has a UIWebView. iOS 7 caches a blank body 304 response, resulting in blank pages being shown when the user refreshes the UIWebView. This is not good user expierience and I'm trying to figure out how to solve this on the iOS side, as I do not have control over how Amazon S3 responds to headers (that's who I use for my resource hosting).

More details of this bug were found by these people: http://tech.vg.no/2013/10/02/ios7-bug-shows-white-page-when-getting-304-not-modified-from-server/

I'd appreciate any help offered to how I can solve this on the app side and not the server side.

Thank you.

Update: fixed this bug using the bounty's suggestion as a guideline:

@property (nonatomic, strong) NSString *lastURL;

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

    if ([self.webView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"].length < 1)
    {
        NSLog(@"Reconstructing request...");
        NSString *uniqueURL = [NSString stringWithFormat:@"%@?t=%@", self.lastURL, [[NSProcessInfo processInfo] globallyUniqueString]];
        [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:uniqueURL] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0]];
    }
}

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    self.lastURL = [request.URL absoluteString];
    return YES;
}
klcjr89
  • 5,862
  • 10
  • 58
  • 91

3 Answers3

2

You can implement a NSURLProtocol, and then in +canonicalRequestForRequest: modify the request to override the cache policy.

This will work for all requests made, including for static resources in the web view which are not normally consulted with the public API delegate.

This is very powerful, and yet, rather easy to implement.

Here is more information: http://nshipster.com/nsurlprotocol/

Reference: https://developer.apple.com/library/ios/documentation/cocoa/reference/foundation/Classes/NSURLProtocol_Class/Reference/Reference.html


Here is an example:

@interface NoCacheProtocol : NSURLProtocol

@end

@implementation NoCacheProtocol

+ (void)load
{
    [NSURLProtocol registerClass:[NoCacheProtocol class]];
}

+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
    if ([NSURLProtocol propertyForKey:@“ProtocolRequest” inRequest:theRequest] == nil) {
        return YES;
    }
    return NO;
}

+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
{
    NSMutableURLRequest* request = [theRequest mutableCopy];
    [request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData];
    //Prevent infinite recursion:
    [NSURLProtocol setProperty:@YES forKey:@"ProtocolRequest" inRequest:request];

    return request;
}

- (void)startLoading
{
    //This is an example and very simple load..

    [NSURLConnection sendAsynchronousRequest:self.request queue:[NSOperationQueue currentQueue] completionHandler:^ (NSURLResponse* response, NSData* data, NSError* error) {
        [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
        [[self client] URLProtocol:self didLoadData:data];
        [[self client] URLProtocolDidFinishLoading:self];
    }];
}

- (void)stopLoading
{
    NSLog(@"something went wrong!");
}

@end
Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • I'm not sure how to integrate this into my UIWebView controller – klcjr89 Feb 16 '14 at 00:41
  • @troop231 This works indirectly with the web view. You register the protocol in the system, and every request made by your app will be consulted with the protocol. In `+canInitWithRequest:` you determine whether the protocol should be consulted on each particular request. – Léo Natan Feb 16 '14 at 00:42
  • Ok so I've started by adding this in my header file: @interface WebViewController : UIViewController And it's showing 8 warnings now – klcjr89 Feb 16 '14 at 00:45
  • @troop231 You need to implement a subclass of `NSURLProtocol`. The client will be provided automatically by the system. I'll edit my answer with an example. – Léo Natan Feb 16 '14 at 00:47
  • Ok I just made the empty subclass .h and .m files. Now I imported the WebProtocol.h into my WebViewController.m, but seem to be missing some things. – klcjr89 Feb 16 '14 at 00:51
  • It seems i cannot set the cache policy in: + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request – klcjr89 Feb 16 '14 at 01:00
  • @troop231 See my example. You need to create a mutable copy and then modify that. – Léo Natan Feb 16 '14 at 01:03
  • Wow, I've got it to work using your code you kindly provided, but it's going into infinite recursion for some reason. – klcjr89 Feb 16 '14 at 01:13
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47578/discussion-between-leo-natan-and-troop231) – Léo Natan Feb 16 '14 at 01:14
1

As the other questions are to use the NSURLConnection every time, which seems like a bit of an overhead: Why don't you execute a small javascript after the page was loaded (complete or incomplete) that can tell you if the page is actually showing? Query for a tag that should be there (say your content div) and give a true/false back using the

[UIWebView stringByEvaluatingJavaScriptFromString:@"document.getElementById('adcHeader')!=null"]

And then, should that return false, you can reload the URL manually using the cache-breaker technique you described yourself:

NSString *uniqueURL = [NSString stringWithFormat:@"%@?t=%d", self.url, [[NSDate date] timeIntervalSince1970]]; 
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:uniqueURL] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0]];

[edit]

based on the discussion in the comments and some of the other answers, I think you might have the best solution manually changing the NSURLCache.

From what I gathered, you're mainly trying to solve a reload/reshow scenario. In that case, query the NSURLCache if a correct response is there, and if not delete the storedvalue before reloading the UIWebView.

[edit 2]

based on your new results, try to delete the NSURLCache when it is corrupted:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache]cachedResponseForRequest:request];

  if (cachedResponse != nil && [[cachedResponse data] length] > 0)
  {
      NSLog(@"%@",cachedResponse.response);
  } else {
    [[NSURLCache sharedURLCache] removeCachedResponseForRequest:request];
  }

  return YES;
}

We might have to refine the check if the cache is invalid again, but in theory this should do the trick!

Duck
  • 34,902
  • 47
  • 248
  • 470
Blitz
  • 5,521
  • 3
  • 35
  • 53
  • This isn't good experience, because the UIWebView will still appear blank until we initiate the cache breaker request. The whole idea is to never show this blank page – klcjr89 Feb 16 '14 at 00:44
  • Wait, does this happen every time for you? From what I read in the linked page, it seems to only happen if the connection was interrupted while downloading the content. In that case, you've a trade-of to make: break the cache always, hence making your apps slower, or only reload the page when necessary, which makes the user experience for the great majority of users better. – Blitz Feb 16 '14 at 00:48
  • It's very easy to replicate this bug due to users who love their refresh button. – klcjr89 Feb 16 '14 at 00:50
  • My experience: that's a typical testcase that users won't have that often. And really, they won't mind the bit longer loading time in those cases. From what i've seen so far, optimize always for the 90%, never for the 10% - and that's what you are doing! – Blitz Feb 16 '14 at 00:53
  • Thats the problem though, there is no longer loading in that case, the UIWebView simply displays an ugly white page due to a crappy iOS 7 bug – klcjr89 Feb 16 '14 at 00:55
  • Is 'webViewDidFinishLoad:' not called or why wouldn't my approach work on the blank page? – Blitz Feb 16 '14 at 00:58
  • webViewDidFinishLoad doesn't always get called if the page is already in the webView internal cache. – klcjr89 Feb 16 '14 at 01:01
  • Okay, that's a bummer - check out the other idea! – Blitz Feb 16 '14 at 01:11
  • I was mistaken, webView didFinishload does get called everytime, including precached pages! So how could I implement your idea to make it look pleasing to the user? – klcjr89 Feb 16 '14 at 20:15
  • See my new update in original question above, I think we may be on to something? – klcjr89 Feb 16 '14 at 20:29
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47606/discussion-between-lordt-and-troop231) – Blitz Feb 16 '14 at 21:15
0
NSURL *URL = [NSURL URLWithString:@"http://mywebsite.com"];    
NSURLRequest *request = [NSURLRequest requestWithURL:URL
                                             cachePolicy:NSURLRequestReloadIgnoringCacheData 
                                         timeoutInterval:30.0];

[myWebView loadRequest: request];

When creating NSURLRequest instance, you can set Cache Policy. Hope it works!

Joey
  • 2,912
  • 2
  • 27
  • 32
  • This only works when the view loads. I need a way to do this every time a request is sent in - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest: – klcjr89 Feb 14 '14 at 01:20
  • 1
    This looks like the only solution: NSString *uniqueURL = [NSString stringWithFormat:@"%@?t=%@", self.url, [[NSProcessInfo processInfo] globallyUniqueString]]; [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:uniqueURL] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0]]; – klcjr89 Feb 14 '14 at 01:33