I am trying to achieve the following on iOS:
- Always load local files to a
UIWebView
for static assets (.html, .js, etc.) - Allow an update protocol such that after some period of time we can return a different set of static assets for the same URLs
Download link for minimal example.
I have this working but it seems the NSURLCache
is sometimes completely missed (reproducible) and the only reliable way to fix this has been to use nasty cachebusting tricks on the page being loaded. Another equally nasty hack is to destroy the UIWebView
and create another.
As an example of this we have three versions of our webapp (red, blue and green for v1, v2 and v3 respectively):
Each screen is made up of a single HTML and JS file.
index.html
----------
<!DOCTYPE html>
<html>
<head>
</head>
<body style="background-color:#FF0000">
<h1 id="label" style="color:#FFFFFF"></h1>
<script src="app.js"></script>
</body>
</html>
app.js
------
var label = document.getElementById('label');
label.innerHTML = 'red';
Every 2 seconds the following happens:
- We change what files the
NSURLCache
will return to make it return the different versions (note we implement this by overridingcachedResponseForRequest:
rather thanstoreCachedResponse:forRequest:
) - The
UIWebView
loads a dummy page "http://www.cacheddemo.co.uk/index.html"
The NSURLCache
logic is implemented simply as a rotating NSMutableArray
:
@implementation ExampleCache
- (id)init
{
self = [super initWithMemoryCapacity:8 * 1024 * 1024 diskCapacity:8 * 1024 * 1024 diskPath:@"webcache.db"];
if(self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(swapCache:) name:@"swapCache" object:nil];
self.cache = [@[@{@"index.html":@"index1.html", @"app.js":@"app1.js"},
@{@"index.html":@"index2.html", @"app.js":@"app2.js"},
@{@"index.html":@"index3.html", @"app.js":@"app3.js"}] mutableCopy];
}
return self;
}
- (void)swapCache:(NSNotification *)notification
{
[self.cache addObject:self.cache[0]];
[self.cache removeObjectAtIndex:0];
}
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
NSString *file = [[[request URL] pathComponents] lastObject];
NSString *mimeType;
if([file hasSuffix:@".html"]) {
mimeType = @"text/html";
} else if([file hasSuffix:@".js"]) {
mimeType = @"application/javascript";
}
if(mimeType) {
NSString *cachedFile = self.cache[0][file];
NSUInteger indexOfDot = [cachedFile rangeOfString:@"."].location;
NSString *path = [[NSBundle mainBundle] pathForResource:[cachedFile substringToIndex:indexOfDot] ofType:[cachedFile substringFromIndex:indexOfDot + 1] inDirectory:@"www"];
NSData *data = [NSData dataWithContentsOfFile:path];
if(data.length) {
NSLog(@"Response returned for %@", file);
NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[request URL] MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
NSCachedURLResponse *response = [[NSCachedURLResponse alloc] initWithResponse:urlResponse data:data];
return response;
}
}
NSLog(@"No response for %@ - %@", file, request);
return nil;
}
The view controller logic uses GCD to reload the UIWebView
after the delay:
- (void)viewDidLoad
{
[super viewDidLoad];
self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[self.webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
[self.webView setDelegate:self];
[self.view addSubview:self.webView];
[self loadContent];
}
- (void)loadContent
{
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@?%d", @"http://www.cacheddemo.co.uk/index.html", arc4random()]]]];
// [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.cacheddemo.co.uk/index.html"]]];
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[[NSNotificationCenter defaultCenter] postNotificationName:@"swapCache" object:nil];
[self loadContent];
});
}
The part I can not understand here is that adding the query string will make the page reloading work flawlessly (loads R - G - B - R - G - ...) - this is the uncommented line:
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@?%d", @"http://www.cacheddemo.co.uk/index.html", arc4random()]]]];
Once we get rid of the query string the NSURLCache
stops being hit other than the first request so it just stays on the R page:
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.cacheddemo.co.uk/index.html"]]];
The fact that the query string causes the NSURLCache
to act as it is supposed to indicates to me that the browser cache is interfering in some way. In my head I figure the caching levels work as so:
- Check browser cache
- If nothing returned so far - check
NSURLCache
- If nothing returned so far - check proxy server cache
- Finally attempt to load the remote resource
How can we disable the browser cache entirely so we can completely control caching behaviour for UIWebView
. Unfortunately I do not see an option to set the Cache-Control
header in NSURLCache
- I already tried returning a NSHTTPURLResponse
with the headers set but this seems to be ignored.
I am not sure I understand correctly, but the cache-control has to be set on server side, with something like no-cache, expires and so on, and not on iOS side.
Second, by modifying the query string i.e.
www.mysite.com/page?id=whatever...
, iOS and any browser think the request is not the same, if you have opened the cache itself with some db editor, you should have seen one request, which is one database entry, for each changed query.This trick of adding a random query string is quite useful for avoiding the browser to cache javascript file.
I hope I understand correctly your question.