iOS WebView remote html with local image files
Asked Answered
D

5

47

Similar questions have been asked before, but I could never find a solution.

Here is my situation - my UIWebView loads a remote html page. The images used in the web pages are known at build time. In order to make the page load faster, I want to package the image files in the iOS application and substitue them at runtime.

[Please note that the html is remote. I always get answers for loading both html and image files from local - I have done that already]

The closest recommendation I got was to use a custom url scheme such as myapp://images/img.png in the html page and in the iOS application, intercept the myapp:// URL with NSURLProtocol subclass and replace the image with a local image. Sounded good in theory, but I haven't come across a complete code example demonstrating this.

I have Java background. I could do this easily for Android using a Custom Content Provider. I am sure a similar solution must exist for iOS/Objective-C. I don't have enough experience in Objective-C to solve it myself in the short timeframe I have.

Any help will be appreciated.

Drew answered 6/4, 2011 at 19:59 Comment(0)
D
86

Ok here is an example how to subclass NSURLProtocol and deliver an image (image1.png) which is already in the bundle. Below is the subclasses' header, the implementation as well as an example how to use it in a viewController(incomplete code) and a local html file(which can be easily exchanged with a remote one). I've called the custom protocol: myapp:// as you can see in the html file at the bottom.

And thanks for the question! I was asking this myself for quite a long time, the time it took to figure this out was worth every second.

EDIT: If someone has difficulties making my code run under the current iOS version, please have a look at the answer from sjs. When I answered the question it was working though. He's pointing out some helpful additions and corrected some issues, so give props to him as well.

This is how it looks in my simulator:

enter image description here

MyCustomURLProtocol.h

@interface MyCustomURLProtocol : NSURLProtocol
{
    NSURLRequest *request;
}

@property (nonatomic, retain) NSURLRequest *request;

@end

MyCustomURLProtocol.m

#import "MyCustomURLProtocol.h"

@implementation MyCustomURLProtocol

@synthesize request;

+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
    if ([theRequest.URL.scheme caseInsensitiveCompare:@"myapp"] == NSOrderedSame) {
        return YES;
    }
    return NO;
}

+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
{
    return theRequest;
}

- (void)startLoading
{
    NSLog(@"%@", request.URL);
    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[request URL] 
                                                        MIMEType:@"image/png" 
                                           expectedContentLength:-1 
                                                textEncodingName:nil];

    NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"image1" ofType:@"png"];  
    NSData *data = [NSData dataWithContentsOfFile:imagePath];

    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [[self client] URLProtocol:self didLoadData:data];
    [[self client] URLProtocolDidFinishLoading:self];
    [response release];
}

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

@end

MyCustomProtocolViewController.h

@interface MyCustomProtocolViewController : UIViewController {
    UIWebView *webView;
}

@property (nonatomic, retain) UIWebView *webView;

@end

MyCustomProtocolViewController.m

...

@implementation MyCustomProtocolViewController

@synthesize webView;

- (void)awakeFromNib
{
    self.webView = [[[UIWebView alloc] initWithFrame:CGRectMake(20, 20, 280, 420)] autorelease];
    [self.view addSubview:webView];
}

- (void)viewDidLoad
{   
    // ----> IMPORTANT!!! :) <----
    [NSURLProtocol registerClass:[MyCustomURLProtocol class]];

    NSString * localHtmlFilePath = [[NSBundle mainBundle] pathForResource:@"file" ofType:@"html"];

    NSString * localHtmlFileURL = [NSString stringWithFormat:@"file://%@", localHtmlFilePath];

    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:localHtmlFileURL]]];

    NSString *html = [NSString stringWithContentsOfFile:localHtmlFilePath encoding:NSUTF8StringEncoding error:nil]; 

    [webView loadHTMLString:html baseURL:nil];
}

file.html

<html>
<body>
    <h1>we are loading a custom protocol</h1>
    <b>image?</b><br/>
    <img src="myapp://image1.png" />
<body>
</html>
Downspout answered 6/4, 2011 at 21:26 Comment(7)
Fantastic! Looks like exactly what I was looking for. I will try this out and let you know.Drew
Nick, I tried your solution and this works exactly as I wanted and as you had described. I am yet to try it out for remote html, but I believe it should work. If you are a freelancer and like to help out I would certainly like to talk to you for some small/micro scale paid services :-) You can reach me at cmsubram at gmail. Thanks, CMDrew
Why is request declared as a property of the subclass? NSURLProtocol already has a request property so you should just be using self.request. In the code above request is always nil.Bagpipe
@sjs Good point, I can't tell you what I've thought when I introduced that property, however there was no harm done, as the example worked fine back then. And no the request is not nil, this is a registered subclass of NSURLProtocol. Look at the static registerClass method's documentation.Downspout
@NickWeaver I have read the documentation. request is never assigned and is nil. I'm using your code and in order to make it work I had to use the property self.request. Try this code today, it does not work. Even if it did work an unused ivar is cruft that should be removed. If you read the documentation you'll also see that -[NSURLProtocol stopLoading] is not an error condition. You shouldn't be logging "Something went wrong!" when that is a regular part of a successful request cycle.Bagpipe
@NickWeaver can i add action in button inside webview?Bleat
@Krutarth Patel Not directly. You will have to add a button/link in your html which triggers loading a certain URL. Then you will have to intercept this in startLoading or in one of the UIWebViewDelegate method such as - webViewDidStartLoad:.Downspout
B
39

Nick Weaver has the right idea but the code in his answer does not work. It breaks some naming conventions as well, never name your own classes with the NS prefix, and follow the convention of capitalizing acronyms such as URL in identifier names. I'll stick w/ his naming in the interest of making this easy to follow.

The changes are subtle but important: lose the unassigned request ivar and instead refer to the the actual request provided by NSURLProtocol and it works fine.

NSURLProtocolCustom.h

@interface NSURLProtocolCustom : NSURLProtocol
@end

NSURLProtocolCustom.m

#import "NSURLProtocolCustom.h"

@implementation NSURLProtocolCustom

+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
    if ([theRequest.URL.scheme caseInsensitiveCompare:@"myapp"] == NSOrderedSame) {
        return YES;
    }
    return NO;
}

+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
{
    return theRequest;
}

- (void)startLoading
{
    NSLog(@"%@", self.request.URL);
    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL 
                                                        MIMEType:@"image/png" 
                                           expectedContentLength:-1 
                                                textEncodingName:nil];

    NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"image1" ofType:@"png"];  
    NSData *data = [NSData dataWithContentsOfFile:imagePath];

    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [[self client] URLProtocol:self didLoadData:data];
    [[self client] URLProtocolDidFinishLoading:self];
    [response release];
}

- (void)stopLoading
{
    NSLog(@"request cancelled. stop loading the response, if possible");
}

@end

The problem with Nick's code is that subclasses of NSURLProtocol do not need to store the request. NSURLProtocol already has the request and you can access with the method -[NSURLProtocol request] or the property of the same name. Since the request ivar in his original code is never assigned it is always nil (and if it was assigned it should have been released somewhere). That code cannot and does not work.

Second, I recommend reading the file data before creating the response and passing [data length] as the expected content length instead of -1.

And finally, -[NSURLProtocol stopLoading] is not necessarily an error, it just means you should stop work on a response, if possible. The user may have cancelled it.

Bagpipe answered 27/11, 2011 at 19:29 Comment(4)
Thanks for this important improvement! I did as you said and it worked.Gadwall
Is there any way to get your app to provide an image requested by a web page loaded in Mobile Safari, or in the UIWebView of another app? will this method work?Quickstep
@Quickstep I don't think so. My understanding is that you cannot intercept http or https requests.Bagpipe
It seems that this solution stopped to work with ios8. Did the cacheStoragePolicies change with iOS8?Alcmene
S
2

I hope I am understanding your problem correctly:

1) load a remote webpage ... and

2) substitute certain remote assets with files within the app/build

Right?


Well, what I am doing is as follows (I use it for videos due to the caching limit of 5MB on Mobile Safari, but I think any other DOM content should work equally):


• create a local (to be compiled with Xcode) HTML page with style tags, for the in-app/build content to be substituted, set to hidden, e.g.:

<div style="display: none;">
<div id="video">
    <video width="614" controls webkit-playsinline>
            <source src="myvideo.mp4">
    </video>
</div>
</div> 


• in the same file supply a content div, e.g.

<div id="content"></div>


• (using jQuery here) load the actual content from the remote server and append your local (Xcode imported asset) to your target div, e.g.

<script src="jquery.js"></script>
<script>
    $(document).ready(function(){
        $("#content").load("http://www.yourserver.com/index-test.html", function(){
               $("#video").appendTo($(this).find("#destination"));           
        });

    });
</script>


• drop the www files (index.html / jquery.js / etc ... use root levels for testing) into the project and connect to target


• the remote HTML file (here located at yourserver.com/index-test.html) having a

<base href="http://www.yourserver.com/">


• as well as a destination div, e.g.

<div id="destination"></div>


• and finally in your Xcode project, load the local HTML into the web view

self.myWebView = [[UIWebView alloc]init];

NSURL *baseURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
NSString *path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[self.myWebView loadHTMLString:content baseURL:baseURL];

Works a treat for me, best in conjunction with https://github.com/rnapier/RNCachingURLProtocol, for offline caching. Hope this helps. F

Sheets answered 5/6, 2014 at 8:36 Comment(0)
C
1

The trick is to provide the explicit base URL to an existing HTML.

Load the HTML into a NSString, use UIWebView's loadHTMLString: baseURL: with the URL into your bundle as the base. For loading HTML into a string, you can use [NSString stringWithContentsOfURL], but that's a synchronous method, and on slow connection it will freeze the device. Using an async request to load the HTML is also possible, but more involved. Read up on NSURLConnection.

Crude answered 6/4, 2011 at 20:36 Comment(0)
N
0

NSURLProtocol is a good choice for UIWebView, but until now the WKWebView still not support it. For WKWebView we can build a local HTTP server to handle the local file request, the GCDWebServer is good for this:

self.webServer = [[GCDWebServer alloc] init];

[self.webServer addDefaultHandlerForMethod:@"GET"
                              requestClass:[GCDWebServerRequest class]
                              processBlock:
 ^GCDWebServerResponse *(GCDWebServerRequest *request)
{
    NSString *fp = request.URL.path;

    if([[NSFileManager defaultManager] fileExistsAtPath:fp]){
        NSData *dt = [NSData dataWithContentsOfFile:fp];

        NSString *ct = nil;
        NSString *ext = request.URL.pathExtension;

        BOOL (^IsExtInSide)(NSArray<NSString *> *) = ^(NSArray<NSString *> *pool){
            NSUInteger index = [pool indexOfObjectWithOptions:NSEnumerationConcurrent
                                                  passingTest:^BOOL(NSString *obj, NSUInteger idx, BOOL *stop) {
                                                      return [ext caseInsensitiveCompare:obj] == NSOrderedSame;
                                                  }];
            BOOL b = (index != NSNotFound);
            return b;
        };

        if(IsExtInSide(@[@"jpg", @"jpeg"])){
            ct = @"image/jpeg";
        }else if(IsExtInSide(@[@"png"])){
            ct = @"image/png";
        }
        //else if(...) // other exts

        return [GCDWebServerDataResponse responseWithData:dt contentType:ct];

    }else{
        return [GCDWebServerResponse responseWithStatusCode:404];
    }

}];

[self.webServer startWithPort:LocalFileServerPort bonjourName:nil];

When specify the file path of the local file, add the local server prefix:

NSString *fp = [[NSBundle mainBundle] pathForResource:@"picture" ofType:@"jpg" inDirectory:@"www"];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://127.0.0.1:%d%@", LocalFileServerPort, fp]];
NSString *str = url.absoluteString;
[self.webViewController executeJavascript:[NSString stringWithFormat:@"updateLocalImage('%@')", str]];
Nonlinearity answered 22/3, 2016 at 4:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.