October 2013 Archives

iOS WebView error and debug logging

Theoretically one of the advantages of implementing a user interface using an embedded UIWebView is that you can debug the whole thing outside of the iOS app, in a regular browser with full debugging facilities. This is true, but sometimes you want to see what's going on in the Javascript code as it's running on the iOS device. Unfortunately, the output from console.log and Javascript error reporting seems to just disappear, rather than go into the debug console in XCode.

Fortunately, this is remarkably easy to fix. It's just a little code on the Objective-C side, and a little on the Javascript side. Note that this is intended for debugging use only. It's a fair amount of unnecessary overhead otherwise.

Note that this uses a custom URL protocol handler. It's small, fast, and a really good way to have Javascript call into Objective-C, asynchronously. While it's easy for Objective-C to call into Javascript using stringByEvaluatingJavaScriptFromString, there is no facility built-in for going the opposite direction.

A few people have played tricks using the location.href, or the href of an invisible iframe or NativeBridge, but I don't really like those solutions. My solution allows you to pass essentially unlimited amounts of data in JSON format both into Objective-C, as well as return structured data.

In my Javascript I used jquery, since I was already using it, but it should be pretty easy to rewrite it without jquery.

Well, here we go:



The Javascript. You'll want to call interceptErrors() from your document ready handler.
function interceptErrors() { // Override console.log console = new Object(); console.log = function(log) { sendToConsoleLogger({k:'c', m:log}); }; console.debug = console.log; console.info = console.log; console.warn = console.log; console.error = console.log; // Override window onerror window.onerror = function(error, url, line) { sendToConsoleLogger({k:'e', m:error, u:url, l:line}); }; } function sendToConsoleLogger(requestData) { $.ajax({ type: 'POST', url: '/_ConsoleLogger', dataType: 'json', data: JSON.stringify(requestData), success: function(responseData) { }, error: function(error) { }}); }

You need to register the URL protocol, presumably in AppDelegate.m, in didFinishLaunchingWithOptions.
[NSURLProtocol registerClass:[ConsoleURLProtocol class]];

In my case, the Javascript isn't even downloaded from the web, it's embedded in the app. And I'm only using this during debugging. Still, when calling from Javascript into Objective-C make sure you take precautions to not allow the Javascript to execute anything it shouldn't. Fortunately, since the Objective-C side is written like you'd write a web service, writing it defensively should be straightforward.

iOS UIWebView for files in bundle without file URLs

I had this little project where I essentially wanted to embed a HTML5/Javascript/CSS web site into an iPad app. The main consideration is that it had to run offline and work for data entry, though later on you could connect to the Internet to upload the data and do additional processing. Yes, for a mass-consumption, high performance app you really have to go native Objective-C, but for the purposes of this app, using a UIWebView would be fine even if it's a little slow.

The first logical choice was to use file:// URLs, and this almost works. The first annoyance comes when you want upload data, where you now need to implement CORS on the server because the request is coming from a source that's not in the same domain (cross-site scripting problem).

The second problem is that HTML5 local storage doesn't work quite right with file URLs either. That problem is rooted in the fact that there isn't a domain, and normally local storage is domain-based.
Actually, I think this might work in an iOS app and Safari, but when I tried it I had another bug that was preventing it from working properly.

In any case, even without this problem this technique is handy, because it mostly eliminates the need for special cases in the Javascript code.

After doing some poking around, I found two interesting solutions:
Rob Napier's RNCachingURL Protocol

RNCachingURLProtocol is sort of the opposite of what I wanted, it makes requests and caches the result locally, which, while useful in some cases, isn't what I wanted. I wanted to always use the version embedded in the main bundle of my app. But I did look at that code quite carefully, and it was integral to my understanding how to do my solution.

AFCache seems like it might do what I wanted, but it's big, complicated, and hasn't been updated in a while. I wanted something small, fast, and ARC-aware.

So here's my solution:

I created two Objective-C classes:

LocalFileURLProtocol: This implements the NSURLProtocol to intercept the requests for files that are in our bundle. Note that we don't use file:// URLs, we use the actual http:// URLs we'd use for online use, but intercept them and return the html/css/js from our app's bundle instead of loading across the network.

LocalURLDecoder: This embeds our local knowledge of our server and URL format, so we know what things to try to intercept. You'll obviously need to customize this for your own app, but it's pretty straightforward.

The only thing you need to do is register the LocalFileURLProtocol, probably in AppDelegate.m in didFinishLaunchingWithOptions:
[NSURLProtocol registerClass:[LocalFileURLProtocol class]];

That's it! Here are the files:





About this Archive

This page is an archive of entries from October 2013 listed from newest to oldest.

August 2013 is the previous archive.

October 2014 is the next archive.

Find recent content on the main index or look in the archives to find all content.