Dr Nic

To WebKit or not to WebKit within your iPhone app?

Oakley's Surf Report

I know HTML. Its on my CV. Expert level. I also know CSS and a whole bunch of JavaScript. I can even do TDD with JavaScript.

And on the iPhone there is this nifty object called UIWebView. Otherwise known as WebKit. Otherwise known as an embedded browser in your iPhone app.

And if you want a sexily awesome looking UI view, like the Today view of the Surf Report app (see right or free on AppStore) that was released on the AppStore recently, then the WebKit is just the best thing since the electric bread slicer for speed of development.

Holy grail of iPhone development?

Well, that’s what we thought. When I chatted with Dan Grigsby last week I mentioned there were good and bad things about the WebKit within an iPhone app.

This article is about good and bad things. The pros and cons. How we managed the integration of the two code-bases. And the answer to the big question: Would we do it again?

Its probably wonderfully useful stuff to know.

Negatives

So first the downsides. It would be remiss of me to fill you full of unbounded promises of easy non-Objective-C victories in your iPhone dev, and not tone them down with a full bucket of wet reality.

Its slow. When the WebKit is first loaded into memory, and we try to do this behind the scenes as soon as we get control of our app from the OS, it can take a good few seconds for your WebKit object to be available. You get notified of its readiness for action via the delegate:

- (void)webViewDidFinishLoad:(UIWebView *)webView

Its slow. You’re running an interpreter (JavaScript runtime) on top of a device with a small CPU and small memory. Go figure.

This reminds me of the fabulous “two minor drawbacks” scene from the 80s British sci-fi comedy Red Dwarf:

CAT: Why don't we drop the defensive shields?
KRYTEN: A superlative suggestion, sir, with just two minor flaws.
  One, we don't have any defensive shields, and
  two, we don't have any defensive shields. Now I realise that,
  technically speaking, that's only one flaw but I thought
  it was such a big one it was worth mentioning twice.

The JavaScript bridge does not appear to block the main thread. This is a good/bad thing. You can invoke JavaScript code within the WebKit via your native Objective-C code.

[webview stringByEvaluatingJavaScriptFromString:@"loadData({some: 'data'})"];

It seems to return control back to your Objective-C code before it has finished executing, so you may need to poll the JavaScript runtime with `isFinished();` calls if you need to know when its complete.

Annoyingly, the Apple documentation for this method suggests otherwise:

JavaScript execution time is limited to 5 seconds for each top-level entry point. If your script executes for more than 5 seconds, Safari stops executing the script. This is likely to occur at a random place in your code, so unintended consequences may result. This limit is imposed because JavaScript execution may cause the main thread to block, so when scripts are running, the user is not able to interact with the webpage.

Quirky.

The JavaScript bridge is one directional. From Objective-C/UIKit you can invoke JavaScript upon the WebKit and henceforth do wonderful things (as per example above).

From JavaScript you have no native nor nice way to invoke methods on Objective-C objects, like you can in the Cocoa implementation of embedded WebKits. What you can do is use custom HTTP protocols, such as surfreport://, to give the OS a way for the webkit to communicate with your app.

We used this for ‘Related Photos’ buttons on some news and athlete’s pages. When you click a button in the webkit, it attempts to redirect to a surfreport:// url. The Objective-C code (your `UIApplicationDelegate`) is notified of this, hides the webkit, and displays the related photos using native UI elements. But we could do anything. Your `UIApplicationDelegate` needs to implement the follow method to receive these requests:

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url

Alternately, you could use JavaScript to invoke this url, possible, via an Ajax call. Haven’t tried it. Might work. Either way, its a dirty hack, and a very annoying situation given that in Cocoa development there is a beautiful two-way bridge. I want that.

Multiple languages in one project. Whilst we mainly just wanted to take a static HTML file, and dynamically update various elements with application data (e.g. the surf and weather conditions in the example above), we needed to do that via JavaScript.

We came up with a consistent API loadData(json) for all pages, as discussed below; yet when something displayed incorrectly we now had an extra possible point of failure: JavaScript, Objective-C, and the original web service data source (e.g. the Surfline data for live updates).

To help isolate issues, we used JavaScript unit testing and a layer of “fixtures” or samples of JSON that might be sent from Objective-C to JavaScript. The tests passed against the fixtures; so if a QA error appeared in the app, we first checked the JSON being sent against the fixtures schema to isolate whether it was a JavaScript error or a data format error. It happened sufficiently often that its worth raising here.

WebKit for rapid prototyping

Nonetheless, the WebKit exists and it is awesome at rendering HTML and CSS, with access to the powerful CSS3 transformations and webkit-specific bonus features.

It is highly likely that your designer can make something beautiful looking in Photoshop and cut it up into HTML + CSS. Comparatively, its highly unlikely they can cut it up into native Objective-C code.

So in the initial phases of application development/prototyping, the WebKit is a sweet option to give the designer on your team direct, immediate access to building the app. If performance is an issue, you later replace it with native UI elements.

Code layout and samples

In our Xcode project, the structure we use for including HTML and associated assets into our apps is:

Classes/           - normal Objective-C .m/.h files
Html/src           - HTML, CSS, and JavaScript files including libraries like jQuery
Html/test          - JavaScript tests
Html/test/fixtures - expected JSON formats to be sent from Objective-C to loadData methods

html and assets

These aren’t Xcode Groups, rather normal filesystem folders. In the Xcode project, the Html/test files are not included nor bundled with the app. The files in Html/src are included in the “Copy Bundle Resources” of the Target (see image) and also linked into the project via a sub-group of “Resources”, so they can be easily accessed within the project.

But when I did the HTML/JavaScript development I worked from the command-line and TextMate, and lived entirely inside the Html/ folder.

For each static HTML page, such as the ‘Today’ view above, there will probably be the following files:

Html/src/today.html
Html/src/today.css
Html/src/today.js
Html/src/jquery.min.js
Html/test/unit/today_test.html

The WebKit will display today.html, which uses normal <link>/<script> to pull in the .css and .js assets. You can quickly see what the page will look like, even before integration into your app, by loading it into the iPhone Simulator (launch the simulator and drag the file in), or even Safari 3.

We settled on a standard API for the primary call from Objective-C into JavaScript: loadData(json). Bare-bones, this method starts off looking like:

// This function is called from Objective-C-land to apply
// the surf information to the HTML template
// +data+ is a hash of key -> value
function loadData(data) {
    $('.data').html('N/A');
    $('img.image_data').removeAttr('src');

    // Assume that any data key can be copied into any HTML element with the same ID
    for (var key in data) {
        var value = data[key];
        var value_exists = fieldExists(value);
        var data_value = value_exists ? value : 'N/A';
        var img_src_value = value_exists ? value : 'unknown';
        $('#' + key).
            filter('.data').html(data_value).end().
            filter('.image_data').attr('src', key + '_' + img_src_value + '.png');
    };
}

What it does is take a data hash like { surf_height_amount: '2-3', surf_height_unit: 'ft' } and HTML like:

<div id="surf_size">
  <span class="data" id="surf_size_amount"></span>
  <span class="data" id="surf_size_unit"></span>
</div>

And the loadData method updates it to:

<div id="surf_size">
  <span class="data" id="surf_size_amount">2-3</span>
  <span class="data" id="surf_size_unit">ft</span>
</div>

So we can build arbitrarily complex HTML templates. By allocating meaningful element IDs (surf_height_amount) and tagging the template elements with class="data" we have a simple mechanism for Objective-C to update the HTML. As the template gets more complex, then so too does the loadData method.

Would we do it again?

Yes.

The WebKit isn’t the holy grail for non-Objective-C developers, but if your grand-poobar level skills are in JavaScript and HTML, and your Objective-C/iPhone skills are still catching up, then its a wonderful prototyping platform. Especially for static, complicated displays of data. Especially if that data includes HTML content from an external feed which needs to be rendered.

For Oakley’s Surf Report app, Anthony is toying with replacing some of the WebKit usage with native UI elements (normal UITableView with custom UITableViewCells) so that we can get back those precious seconds and give them to the user as a Christmas present.

Related posts:

  1. iPhone dev podcast about fmdb-migration-manager and rbiphonetest Radio is where ugly people go. Podcasts is where ugly,...
  2. Unit Testing iPhone apps with Ruby: rbiphonetest Everything to love about Ruby: the concise, powerful language;...
  3. Prototype: “element-id”.$() instead of $(‘element-id’) The Prototype library gives us the $() operation for converting...
  4. Extend Prototype $() yourself If you’re using the prototype javascript library, its fun to...
  5. Ajax on Rails – Prototype vs JQuery [Original article published on DevLounge - please post comments there]...

44 Responses to “To WebKit or not to WebKit within your iPhone app?”

  1. Matt Conway says:

    We’ve done a fair amount of webkit embedding as well, and your comments are spot on. One additional item of note is logging from javascript – one would think console.log would send to the same place as NSLog from objective-c when running within embedded webkit (and normal console.log otherwise). This is not the case. I’ve submitted a bug into the abyss that is Apple’s bug reporting system.

    So we implemented our own logger that we inject into whatever document we load within webkit. We tried using the callback mechanism by setting document.location to a custom url, and this worked at first but would cause weird behavior depending on where/when it was called, and so we ended up settling on a polling mechanism.

    From a class extending UIWebView:

    - (id)initWithCoder:(NSCoder *)coder
    {
      if (self = [super initWithCoder:coder]) {
      self.delegate = self;
      #ifdef DEBUG
        [self createJavascriptLogger];
      #endif
      }
      return self;
    }
    
    - (void) createJavascriptLogger
    {
      NSLog(@"Adding javascript logger to webview %@", self);
      NSString *createLoggerJS = @"console = { msgs: new Array(), log: function(msg) { console.msgs.push(msg); }, shift: function() { return console.msgs.shift(); } };";
      [self stringByEvaluatingJavaScriptFromString:createLoggerJS];
      [self readJavascriptLogger];
    }
    
    - (void) readJavascriptLogger
    {
      NSString *msg = NULL;
      while( (msg = [self stringByEvaluatingJavaScriptFromString:@"console.shift();"]) != NULL && [msg length] != 0)
      {
        NSLog(@"JS: %@", msg);
      }
      [self performSelector:@selector(readJavascriptLogger) withObject:nil afterDelay:0.1];
    }
  2. Dr Nic says:

    @Matt – oh yeah debugging is awful. Thankfully I wrote all the javascript loadData with unit tests (e.g. all $('.data') elements must have a value assigned to them), with a fixtures file specifying demo json being passed. So we could find all bugs via Safari/Firefox.

    If any bugs turned up when the app was run in the simulator or on the iphone I just assumed that the Objective-C code was sending through the wrong json data formats. My code had tests :)

  3. Dr Nic says:

    PS. We’ll definitely steal your code for future work.

  4. [...] Dr Nic » To WebKit or not to WebKit within your iPhone app? (tags: iphone) [...]

  5. Colin says:

    Coming from a guy that completely understands what you just said, but doesn’t actually do it himself and instead chooses to leave it to the professionals….

    So that’s how you did it!
    Either way the resulting view is great. I just want it faster. :)

  6. Sebastian says:

    Very usefull post. Thank you!

  7. Don Hopkins says:

    Very interesting and useful stuff — thanks for sharing it!

    I’ve been going down the same road.

    It’s very interesting to know (without having to perform the experiment myself) that stringByEvaluatingJavaScriptFromString is asynchronous. I was wondering about that.

    I have been calling back to my iPhone app by implementing:

    - (BOOL)webView:(UIWebView *)webView0
    shouldStartLoadWithRequest:(NSURLRequest *)request
    navigationType:(UIWebViewNavigationType)navigationType

    And that worked fine. I wrote a function to parse the URL query parameters into an NSMutableDictionary, look up my handler by the url path, and pass the params to my handler.
    That handler would do its thing, and return “NO” so stop the URL from being loaded.
    But now I need to return data from the URL handler, like JSON (or JSONP which is JSON wrapped in a method cal do actually do something with the data).
    So I have implemented NSURLProtocol, which is able to return data to the client.
    And it seems to work just fine! Now I can load JSON by appending script tags to the document that load dynamically generated JSONP data, which calls back to a JavaScript function to handle the data.

    How are your experiences using jquery in WebKit?
    Does it work well, and is it small and fast enough to be useful in that context?

    Thanks a lot for sharing what you’ve learned!

    -Don
    (dhopkins@DonHopkins.com)

  8. Mike Rundle says:

    One interesting thing I’ve found is that since you’re loading HTML files from within your application’s bundle, MobileSafari allows you to do cross-domain Ajax calls without a problem (everything would be cross-domain from a file:/// URL, probably why they allow it).

    As to the loading problem, one way I’ve tried to work-around that, at least visually, is to load a UIImage at a higher z-index than the UIWebView, and then once didFinishLoading gets fired, I fade out the image and the HTML/CSS shows through. Not a fully solution obviously, but I think it helps a bit since the flash from your UIWebView’s background color to the loaded page is a little startling, plus the 1-2 second loading time factor.

  9. Stephen Ponce says:

    Great article!

  10. [...] Nic has shared his experiences using WebKit to implement part of your iPhone app (in his case [...]

  11. Dave Johnson says:

    You should also check out PhoneGap – an open source project that exposes IPhone APIs to web developers: http://phonegap.com/

  12. [...] Nic has shared his experiences using WebKit to implement part of your iPhone app (in his case [...]

  13. [...] Nic has shared his experiences using WebKit to implement part of your iPhone app (in his case [...]

  14. [...] Nic has shared his experiences using WebKit to implement part of your iPhone app (in his case [...]

  15. [...] pÃ¥ din iphone (f.eks kameraet eller gps enheden) . Dette kan du fÃ¥ adgang ved at benytte webkit inde fra en native app (webkit er safaris rendering engine). Filed in iphone « behandling af ekstreme datasæt [...]

  16. Fabien Penso says:

    Hey DrNic,

    Very nice application you made with Surf Report, I like the very slick design which still stays within the Apple UI boundaries (tableviews etc).

    Great work, will be one I’ll look at for my next apps.

  17. Perenzo says:

    Here you might find even more code showing Objective-C/ Javascript communication: http://code.google.com/p/big5/

  18. [...] other day “Dr Nic” wrote up an article on how he had used a WebKit instance (along with HTML, CSS, and JavaScript) to render a portion of his iPhone [...]

  19. [...] Dr Nic » To WebKit or not to WebKit within your iPhone app? (tags: content tutorial iphone webkit embedded) This was written by gkamp. Posted on Wednesday, November 19, 2008, at 2:41 am. Filed under Daily Deli. Bookmark the permalink. Follow comments here with the RSS feed. Post a comment or leave a trackback. [...]

  20. [...] Nic”写了篇关于他如何用HTML, CSS, and JavaScript创建一个WebKit实例 移植他的一个iPhone的功能. [...]

  21. [...] Nic”写了篇关于他如何用HTML, CSS, and JavaScript创建一个WebKit实例 移植他的一个iPhone的功能. [...]

  22. [...] Thereby UiWebView isn’t always the best solution. A good read on this matter would be To WebKit or not to WebKit. Also writing a native iPhone app is a great oppurtunity to learn and add a(nother) programming [...]

  23. Adrian says:

    Hey, Great article.

    I am trying to build an app that displays high resolution image files and allows users to zoom right in.

    The problem is that the standard UIWebView (which we are currently using) is not adequate, as the images are too big for it to display.

    If you have any ideas, I’d be really appreciative.

    Cheers!

  24. [...] Dr Nic » To WebKit or not to WebKit within your iPhone app? (tags: programming development webdev code tutorial reference javascript css cocoa html objective-c iphone embedded webkit) [...]

  25. vlad says:

    Well, better way is use rhodes framework (WebView+web server+ruby) running locally on the device :)

  26. Mark says:

    Why the mismatch between the data and the HTML? In one, you have surf_height_amount, and in the other, you have surf_size_amount. Is there some magic going on I don’t understand, or is that a typo? Same thing with surf_height_units and surf_size_units.

    Also is the id surf_size ever used? I don’t see any place where it is being used, and yet I can’t get the code to work.

  27. Dr Nic says:

    @Mark, they are probably typos in extracting the samples from the original Surf Report app and cutting it back for example purposes. Does it need cleaning up to make sense or is it obvious what it should be?

  28. [...] To WebKit or not to WebKit within your iPhone app? [...]

  29. [...] To WebKit or not to WebKit within your iPhone app? [...]

  30. Andy says:

    Sorry if I’m blind, but I cant seem to find the link to the
    example xcode project for this post. I’m trying to do something
    nearly identical to what you describe and your article is
    the only source I’ve found on the internet describing this
    in detail.. So, thanks, and wheres the cake? :)

  31. Dr Nic says:

    @Andy – we dumped the sample code here http://github.com/mocra/webtouch/tree

  32. Andy says:

    I see, thank you!

    I’ve made progress on my project, but I’m running into the non-blocking
    aspect of the JS I’m calling. Any tips on doing polling within ObjC, so I can
    ask the uiwebview when its done, since apple doesnt offer the two way
    bridge in cocoa touch? The webViewDidFinishLoad delegate method doesnt
    do me any good here, as the JS isnt finished yet when this method runs.
    My cocoa-fu is still rather weak, so its probably a novice question.

  33. Dr Nic says:

    @andy – polling is probably it afaik

  34. Amar Jadhav says:

    i am trying to implement shouldStartLoadWithRequest.
    But i am not able to generate click event in iphone simulator. is there any specific way to generate this event in the simulator.
    Please help.

  35. [...] other day “Dr Nic” wrote up an article on how he had used a WebKit instance (along with HTML, CSS, and JavaScript) to render a portion of his iPhone [...]

  36. [...] 5 days ago iPhone Development – First Impressions First saved by jeeves2001 | 5 days ago To WebKit or not to WebKit within your iPhone app? First saved by lazzyjeff | 9 days ago Clutter 0.7.0 Developers Release First saved by mycita | [...]

  37. [...] [upmod] [downmod] Dr Nic » To WebKit or not to WebKit within your iPhone app? (drnicwilliams.com) 1 points posted 6 months ago by jeethu tags webkit development iphone [...]

  38. gene tsai says:

    one note about stringByEvaluatingJavascriptString bring asynchronous. this is NOT true, if you do it correctly.

    somewhere in the comments, there is a guy calling a function via stringbyevaluatingjavascript string, then having the js in the page call a url scheme myscheme://{… json data …} to return the data.

    I initially had the same problem, but I discovered that this works :

    NSString *js =@{(function(){\
    var obj = doComplicatedThing();
    return JSON.encode(obj);
    })();};
    NSString * output = [webView stringByEvaluatingJavascriptString:js];

    and output DOES contain the json return value!!

    the trick here is that you need to wrap the js function in a closure
    “(function(){return something})();”

  39. [...] Webkit, the brower layout engine for Safari, is explored in this post, although it is directed at a more intermediate level of developers or, more specifically, [...]

  40. [...] course, there are lots of other things to consider, like whether or not to use WebKit. For information on the iUI, I came across this fantastic resource.  Finally, for information on [...]

  41. [...] Dr Nic's To WebKit or not to WebKit within your iPhone app? (tags: javascript iphone development) [...]

  42. [...] WebKit within your iPhone app? Filed under: 분류되지 않음 — hamjii @ 4:33 오후 Dr Nic’s To WebKit or not to WebKit within your iPhone app?. 덧글 [...]

  43. Jollyprez says:

    I use UIWebView all over the place. My first app ( an eBook ) is ALL webkit. Works great.

    I wanted to note that it IS possible to communicate FROM the javascript on an html page back to cocoa using the UIWebView protocol: shouldStartLoadWithRequest. Works great as long as you don’t try to do too much with it.

    The BIG win with having your detailed pages be rendered with a UIWebView is that standard tools, such as DreamWeaver, can be used to create the pages and fully trick it out. I have a test page, sized to emulate iPhone and iPad to see exactly what they look like within the device.

    The artists and designers can then change everything to their hearts’ contents.