My latest contract project has me doing a bunch of custom work with a UIWebView: we have XHTML content that we want to render in our app, but with some fairly extensive changes to its presentation, such as paginating the content like a book, and intercepting taps on links. Given the option of using and customizing the built-in WebKit rendering, versus parsing the XHTML myself, laying it out, etc., the choice was a no-brainer.

The trick, then, is in how to extend and customize the functionality. As a long-time curly-brace application developer, my natural instinct is to impose control from the Cocoa side, perhaps by subclassing UIWebView to achieve custom behavior (although this is specifically discouraged by the documentation), tying in delegates where possible, perhaps even employing some render hackery (like using an offscreen UIWebView and then blitting its pixels into some visible view). But this really isn’t the right way to do it: for starters, it still gives you no access to the DOM, which is where nearly all the value of your HTML content is.

I suspect younger readers already know what the right answer is: insert your own JavaScript, and work inside the UIWebView. This is pretty straightforward to do — you can load the HTML source into a string and then munge it as necessary, such as by strategically adding references to your own CSS stylesheets or JavaScript (.js) files, and then load that modified source into the UIWebView, along with an appropriate base URL to help resolve relative paths. I say this would be a natural conclusion for younger developers because I suspect that most young developers start with either Flash or JavaScript, as these environments deliver immediate visual results and aren’t hard to get into (plus, JavaScript is free). Developers my age sometimes wish that computers still came with an introductory programming environment like a flavor of BASIC or HyperCard, overlooking the fact that today’s dominant starter language is included with every browser.

What makes JavaScript programming practical in an iOS app is the method -[UIWebView stringByEvaluatingJavaScriptFromString:], which does exactly what the wordy method name says: it tells a UIWebView to execute arbitrary JavaScript contained in an NSString parameter, and return the result as an NSString. You can easily try it out on a UIWebView in your own application like so:

	[myWebView stringByEvaluatingJavaScriptFromString:
		@"alert (\"hello JavaScript\");"];

With this door opened between the JavaScript and Cocoa worlds, you have two-way access to the DOM and how it is rendered. For example, you can pull out the web view’s selected text by writing a simple JavaScript function, and calling it from Cocoa. Or slurp it all in by walking the DOM, appending all the textContent, and returning one big NSString. Or collect all the links with document.elementsByTagName('a') and index them in Cocoa.

But don’t stop there. With the richness of JavaScript, you can employ all the tricks you see in rich web applications, such as rewriting the DOM on the fly, scrolling around programmatically and finding element coordinates, etc. Plus, since you’re only running against one pseudo-browser (iOS’ built-in WebKit framework), you don’t have to work around the incompatibilities and quirks of multiple browsers like regular web developers do.

As I’ve worked on this project, I’ve settled into a “Render unto Caesar…” strategy. Meaning that anytime I need to access contents of the DOM, or change how it is rendered, I know I’m writing a JavaScript function, because that’s what owns the web content and its rendering. For the rest of the app, it’s still Cocoa.

There are hard parts, not the least of which is that fact that I’m still pretty green when it comes to JavaScript and messing around with the DOM. Mozilla Dev Center is a very useful resource for this, despite some search and link breakage, far more so than Apple’s Safari Dev Center.

The other problem is that debugging JavaScript in a UIWebView is notoriously difficult. Unlike desktop browsers, there is no “developer mode” you can drop into in order to inspect elements, see the results of your actions, or even log messages efficiently. Worse, the smallest syntax error will break execution of your function (and major syntax errors can break your whole .js file), so there is often little recourse but to jam alert() calls into your code just to see how far the interpreter gets before quietly dying. That said, iOS Safari behaves very much like its desktop equivalent, so it is possible to do some amount of development and debugging in desktop Safari’s developer mode, or the WebKit nightly build, before switching back to the UIWebView. That said, I only do this for extreme cases of debugging — I wouldn’t want to develop a bunch of new code against WebKit nightly only to find it doesn’t work the same way on the production version of iOS.

Anyone with a browser knows how rich web applications can be, and so you implicitly know that anything you can do in Safari can also be done inside a UIWebView. Sometimes it makes sense to do exactly that.

1 Comment

  • 1.… replies at 13th November 2010 um 7:24 pm :

    Hi Chris,
    A couple of things you can try to make your life easier:
    1) You might be able to use Firebug Lite inside your UIWebView (on an iPad)
    2) You can use the shouldStartLoadWithRequest method of UIWebViewDelegate to pass logging messages from your javascript via psuedo-urls (you’ll need to serialize them with a queue and a callback)

Leave a comment

You must be logged in to post a comment.