I implemented a custom NSURLProtocol that allows me to use a static zipped version of a website as a target for a webView. It opens the zip on the go and load the required data.
But the issue is that NSURLProtocol seems not to behave properly with relative paths ? That is I have the following structure :
assets/css/main.css
assets/css/style.css
assets/images/sprite.png
index.html
And call sprite.png from the css using : background: url(../images/sprite.png) no-repeat;
but, the requestURL in my custom NSURLProtocol shows scheme://host/images/sprite.png, missing the assets part. It works fine if I switch the .. part for assets, but I would rather not have to do this.
I found the same issue here : Loading resources from relative paths through NSURLProtocol subclass but this got no answer.
I couldn't find any way to either fix this issue so that the request properly resolves the relative path, or fix the path myself afterwards (But i would need to know where the request originated from, and had no luck there either)
Any help appreciated, thanks in advance.
Side note :
Same problem using #import url("style.css"); in main.css
Edit :
I start by downloading the zip file from a remote server :
NSURL * fetchURL = [NSURL URLWithString:zipURLString];
[…]
NSString * filePath = [[self documentsDirectory] stringByAppendingPathComponent:fetchURL.path.lastPathComponent];
[zipData writeToFile:filePath atomically:YES];
So, from http://host/foo/archive.zip, i save it to documentsDirectory/archive.zip.
From there, I change the scheme and the url to point on the zip file :
NSString * str = [NSString stringWithFormat:#"myzip://%#", zipURL.path.lastPathComponent];
[_webView loadRequest:[NSURLRequest str]];
Which opens myzip://archive.zip, and if no such file was found in the zip file, I append /index.html to the current path.
Thus the following requests arrive in my NSURLProtocol subclass - (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id < NSURLProtocolClient >)client:
myzip://archive.zip (Changed to myzip://archive.zip/index.html)
myzip://archive.zip/assets/css/main.css
myzip://archive.zip/styles.css (Problem here)
Finally fixed it.
I had the following in my NSURLProtocol :
- (void)startLoading {
[self.client URLProtocol:self
didReceiveResponse:[[NSURLResponse alloc] init]
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
//Some other stuff
}
and solved the issue with the following :
- (void)startLoading {
[self.client URLProtocol:self
didReceiveResponse:[[NSURLResponse alloc] initWithURL:_lastReqURL MIMEType:nil expectedContentLength:-1 textEncodingName:nil]
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
//Some other stuff
}
Where _lastReqURL is _lastReqURL = request.URL;, from
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id < NSURLProtocolClient >)client {
self = [super initWithRequest:request cachedResponse:cachedResponse client:client];
if (self) {
_lastReqURL = request.URL;
// Some stuff
}
}
I can only assume the URL part in NSURLResponse is critical when dealing with relative-paths (seems logical).
I think this might refer to the way you do load the Request or the HTML.
Could you paste the code for your request? I guess, you load the HTML locally, so don't forget to set the baseURL accordingly, else the relative pathes won't work anymore:
For example the following:
[self.webView loadHTMLString:html baseURL:[NSURL URLWithString:#"host"]];
Related
I have a UIWebView and I make it load an HTML string. The string contains http urls to images. The images get loaded, but shouldStartLoadWithRequest: method is not called for them. It is called just once, for about : blank. However all the image requests are successfully passed into my NSURLCache subclass, into cachedResponseForRequest: method. But I would like to handle the requests in the shouldStartLoadWithRequest: method. Can anything be done about that? How can I make these requests go into the delegate method?
Here is the code:
#interface URLCache : NSURLCache
#end
#implementation URLCache
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
NSLog(#"REQUEST = %#", request.URL);
return [super cachedResponseForRequest:request];
}
#end
#interface ViewController ()<UIWebViewDelegate>
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[NSURLCache setSharedURLCache:[URLCache new]];
NSString* path = [[NSBundle mainBundle] pathForResource:#"test" ofType:#"html"];
NSString* html = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:nil];
self.webView.delegate = self;
[self.webView loadHTMLString:html baseURL:nil];
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSLog(#"URL = %#", request.URL);
return YES;
}
#end
The shouldStartLoadWithRequest method is called only for page loads (that is, when the page as a whole gets loaded or when a frame's contents get loaded), not when loading individual resources such as images, JavaScript, or CSS.
If you want to manipulate all HTTP/HTTPS requests from a UIWebView, the only way I'm aware of to do so is by using a custom NSURLProtocol subclass. See Apple's Custom HTTP Protocol sample code project for an example of how to do this.
I'm developing an Epub Reader for Ios, notice that i don't use any epub library and i parse the epub myself.
I needed a method for loading resource from epub into the UIWebView, for example images or CSS file ... (resource that requested in the html files).
for this purpose i decided to use NSURLCache class and override it's method (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
so i can intercept the request and find the right data from epub then create a NSCachedURLResponse and return it.
so far so good. BUT the the problem is that the data (i.e images) wont show in the webview. look at below code:
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
/*
the request.URL is something like this applewebdata://UUID/OEBPS/Images/cover.jpg ,
so the getResourcePathFromRequest give me the path OEBPS/Images/cover.jpg,
so i can find the right data in epub
*/
NSString *resourcePath = [self getResourcePathFromRequest:request.URL];
/*
ResourceResponse is a class contaning data and mimeType,
i tested this and the data and mimeType are good so this is not the problem
*/
ResourceResponse *response = [[self getBook] fetch:resourcePath];
NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[request URL] MIMEType:response.mMimeType expectedContentLength:[response.mData length] textEncodingName:#"UTF-8"];
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:urlResponse data:response.mData];
return cachedResponse;
}
at this point everything looks normal, the method getting called, the right data being found and the response object return, but it wont load the image into the webview.
so what is the problem?
tnx in advance.
NOTE: i'm new in ios programming world (coming from android:)
I loaded the data with base url (http://localhost) and everything went fine.
I'm an iOS newb (.NET professional), so this may be a simple issue but I couldn't find anything through the SO search or Google (and maybe not looking for the right terms).
I'm writing an app that displays information from a DD-WRT router through it's web interface. I have no problem displaying the initial page and navigating through any of the other pages, but if I make any change on a form (and it redirects to apply.cgi or applyuser.cgi), the UIWebView is blank - it's supposed to display the same page, with the form submission changes. The site works fine in Mobile Safari, which I find intriguing, but I guess UIWebView isn't totally the same.
I think the iOS code is pretty standard for display a webpage, but I'll list it below. I can't give you access to my router because, well, that's not a good idea :) Hopefully someone with a DD-WRT router can help (or know what my issue is anyway).
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *sURL = #"http://user:pass#XXX.XXX.X.X";
NSURL *url = [NSURL URLWithString:sURL];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
self.webView.delegate = self ;
}
And I'm doing a few things with Javascript in the webViewDidFinishLoad method, but I know that's not the culprit because it still happens when I comment it out.
Well I figured out the problem on my own. I think part of it was putting the username & password in the URL (which was just a temporary measure) because I found that method provided the same results in mobile Safari and desktop Chrome.
So I added MKNetworkKit to my project that provided a simple way to add authentication to my request, and found I had to make a specific request to POST the data, then reloaded the page the to see the changes.
In the (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType method, I check if ([request.HTTPMethod isEqualToString:#"POST"]) and do this:
NSString *sPostData = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
NSArray *aPostData = [sPostData componentsSeparatedByString:#"&"];
NSMutableDictionary *dPostData = [[NSMutableDictionary alloc] init];
//i don't know if this is the best way to set a dictionary, but it works
for (id apd in aPostData)
{
NSString *key = [apd componentsSeparatedByString:#"="][0];
NSString *val = [apd componentsSeparatedByString:#"="][1];
[dPostData setValue:val forKey:key];
}
MKNetworkEngine *engine = [[MKNetworkEngine alloc] init];
MKNetworkOperation *op = [engine operationWithURLString:[request.URL description] params:dPostData httpMethod:#"POST"];
[op setUsername:#"myUserName" password:#"myPassword" basicAuth:YES];
self.postedRequest = TRUE; //a bool I set so, when it comes to webViewDidFinishLoad, I reload the current page
[op start]; //send POST operation
I am loading a local HTML into a UIWebView as follows :
- (void)viewDidLoad
{
[super viewDidLoad];
//Setup our UIWebView
webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.delegate = self;
webView.scalesPageToFit = YES;
webView.userInteractionEnabled = YES;
[self.view addSubview:webView];
...
[self loadWebPage];
}
- (void)loadWebPage {
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"index" ofType:#"html"]];
[webView loadRequest:[NSURLRequest requestWithURL:url]];
}
And in the header :
#interface ViewController : UIViewController <UIWebViewDelegate>
However for some reason my webview delegates are not being called :
- (void)webViewDidStartLoad:(UIWebView *)webView {
NSLog(#"Starting HTML Load Process");
}
- (void)webViewDidFinishLoad:(UIWebView *)webViewRef {
NSLog(#"Web View Finished Loading Method");
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
NSLog(#"Error loading HTML ! %#", error);
}
Can anyone suggest why ?
Thanks !
Called the method [self loadWeb];
But on definition the method is - (void)loadWebPage;
Is it typo?
The only way I can reproduce is this by declaring the webView #property as weak. Can you confirm you are declaring it as a strong reference?
how did you declare your UIWebView instance variable, as a strong or weak? If weak make it strong.
The problem is that this call.
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"index" ofType:#"html"]];
Looks like you're loading the HTML from inside your bundle. This means you need to confirm that all the additional files (.js, .css, and any media files) also need to be present in your bundle.
So the first thing need to check fileURLWithPath: point either it's returning nil or not. If it's not then it means that .html page resolved, however the additional files not resolved as part of the request because it's not placed on the same "index.html" path.
First you need to confirm the directory structure of your "index.html" and all the additional files used inside the page. In order to do that, you have to right click on binary .app in your "build" directory in the finder and then select "Show Package contents".
binary (.app) > right click > Show Package contents
This shows what the app's main bundle contains. Look inside for your HTML file. If it's in a folder, that folder name needs to be in the inDirectory parameter of the pathForResource call. If it's on the top-level then you don't have a folder reference. Either delete & re-drag it into project or use the pathForResource version without inDirectory.
The best way to don't setup the base path or relative path for the resources inside .html page (.js, .css and images) because it don't work. Copy all the additional resources next to "index.html" file so it would be easy to resolve on runtime from bundle.
NSString *path = #"<Index.html Path>";
NSURL *url = [NSURL URLWithString: [path lastPathComponent]
relativeToURL: [NSURL fileURLWithPath: [path stringByDeletingLastPathComponent]
isDirectory: YES]];
Try this
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"index" ofType:#"html"]isDirectory:NO];
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 overriding cachedResponseForRequest: rather than storeCachedResponse: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.
The browser does appear to have its own "cache" above the networking layer's cache (i.e. NSURLCache).
Not sure if setting the cache control headers will solve it but if you want to try that you can do so in your code for cachedResponseForRequest. Where you create an NSURLResponse object you can use initWithURL:statusCode:HTTPVersion:headerFields: to set the header fields (including the cache control headers). In your cache control header you can try using both no-store and no-cache (e.g. http://blog.55minutes.com/2011/10/how-to-defeat-the-browser-back-button-cache/ ). If you try this please let us know if it does or does not work.
However, in practice I think the most pragmatic and maintainable solution will be to the use a number on the URL (i.e. cache busting). BTW instead of a random number you can use a simple increment that gets reset whenever the webview is allocated.