Developing hybrid iPhone apps

Posted by Dave on November 25th, 2009

There’s been much discussion recently about native iPhone apps and web apps, and whether one approach is better than the other.  I’d like to suggest a third way of developing for the iPhone, namely “hybrid” iPhone apps.

A hybrid app is a native iPhone app providing a wrapper to embedded HTML / JavaScript / CSS content.  I released a hybrid app (in collaboration with JavaScript guru Tim Down) on day one of the App Store.

Our hybrid app is a simple puzzle game called iDrops, based on the classic SameGame concept.  Tim originally wrote the game in JavaScript some years ago, with myself providing the visuals.  So we already had an interoperable, multi-browser-supporting JavaScript game to hand.  With the iPhone SDK still in its infancy, we decided to take our existing code and wrap it up as a native app.  What we ended up with was the same game, based on the same HTML / JavaScript/ CSS code, in a format that we could sell on the App Store.

In order to get a hybrid game app up and running, there were several problems we had to solve.  This article describes how we did so.

Passing native events to the web page

Like any app, we need all user interaction to be passed to the underlying interface code – in our case an embedded web page.  We started out with a native app containing just a web view.  We quickly ran into problems when trying to create a single-screen fixed-size non-scrolling game.  It turns out that UIWebView will scroll even if your page fits perfectly within the iPhone’s 320 by 480px window, and nothing we tried would disable this scrolling.  (UIWebView is subclassed from UIView, not UIScrollView.)

To work around this, we added our own event detection layer.  The app now has a single view controller, containing a single web view, with a custom event-catcher view positioned on top. The event-catcher view has a custom touchesBegan:withEvent: method, which detects the position of any single-touch events, and converts these into JavaScript function calls for the embedded code.

Whenever the user touches a point on the iPhone’s screen, the event-catcher view detects the location of the event.  Based on the x and y values of the event, it calls an appropriate JavaScript function within the web view, using UIWebView’s stringByEvaluatingJavaScriptFromString method.  This provides an easy way to pass user interaction events into the embedded JS code.

Passing web events back to the native app

This was much harder to achieve.  All of our game logic is contained within the HTML and JavaScript code, but in some cases we wanted to use a native iPhone view instead of an HTML one.  For example, if you beat your previous high score, we wanted to display a native UIAlertView to congratulate you.

To achieve this, we needed a way for the JS code to pass events back to the native app code.  Unfortunately UIWebView doesn’t provide a way for embedded JS to call out to native code.  Events can be passed from native to JS, but can’t be passed back the other way.

Our workaround was to make use of the web view’s webView:didFailLoadWithError: method to pass events back to the native code.  When our web view first gets created, it loads a frameset containing a 100%-height frame (for the actual game code), and a 0%-height hidden frame.  Whenever we want to pass an event back to the native code, the JS game code attempts to load a nonexistent file into the hidden frame.  The name of the file is the name of the event we want to pass back to the native code.  Since the file doesn’t exist, the load event triggers a didFailLoadWithError in the native web view’s delegate.  Our didFailLoadWithError event handler then uses the URL of the nonexistent file to detect the nature of the event, and triggers the corresponding native UI event (such as displaying an alert view).

This approach works surprisingly well, with one major limitation.  In order to be able to rely on this approach, the native code should take control of user interaction whenever a message is received by didFailLoadWithError.  Otherwise, it’s always possible that the user could trigger another event on the embedded web page, and trigger new didFailLoadWithError events before the previous events have been processed.  Since we only ever use these events to display modal views such as UIAlertViews and UIActionSheets, this isn’t a problem for our app, as the modal views take control as soon as the first event is received and prevent any further events from being handed over to the embedded code.

Debugging

Recent versions of (desktop) Safari include a very good JS debugger and development menu.  This was available in WebKit during the development of iDrops, and provided a great way for us to debug the HTML / JS code outside of the UIWebView environment.  Because Safari and UIWebView are based on the same engine, we could be reasonably confident that changes made in one would behave the same in the other, and much of our heavy debugging work could be done outside of the embedded environment, speeding things up considerably.  We could also test any changes against desktop Safari by changing Safari’s user agent string from the Develop menu to switch between the embedded and non-embedded rendering of the game.

Once the HTML / JS CSS code worked well in Safari, we would then test the same code in the embedded environment.  We used the “hidden frame” approach to log debug messages from within the embedded JavaScript code.  To log a debug message, the JS code tries to load a non-existent “log” page into the hidden frame, with an appended query string containing a URL-encoded log message.  Back in the native code, didFailLoadWithError detects the log event, decodes the log message, and passes it to NSLog.

We created a custom iPhone logger for Tim’s excellent log4javascript library, and used this to fire off the debug messages from JavaScript.  If we logged too quickly, then log events would sometimes be lost (as more errors would occur than could be processed in time), but for most logging scenarios, this worked well.

Local data storage

UIWebView provides support for HTML 5’s client-side database storage feature.  This means that we can code for standards-compliant database storage, safe in the knowledge that this will work in any HTML 5-compliant browser, and also in our embedded iPhone code.  We use client-side database storage for saving and restoring game state (saving JSON representations of the game state into the database), and also for tracking the user’s high score.  This “write once, use anywhere” approach to data storage is another benefit to developing hybrid applications.

Localisation

Our native iPhone app, and the HTML / JS it wraps, are localised into five different languages (English, French, Spanish, Italian and German).  We achieved this by having five different copies of the frameset mentioned above (frameset_en.html, frameset_fr.html, etc.), and calling the appropriate frameset based on the iPhone’s current system language.  The frameset then passes the current localisation over to the JS code, which uses the appropriate localised text on the web page.  For the native alerts, we just use multiple Localizable.strings files in Xcode as normal.

Device orientation

We wanted the game to operate in two different orientations (landscape and portrait).  This was implemented with a custom willRotateToInterfaceOrientation:duration: method in the native code, which passes the new orientation value on to the JS code whenever the device is rotated.  The JS code then re-formats the HTML to match the new orientation.  Unfortunately we had to turn off animation for the rotation (it was too jerky), but this still gives a way to support multiple iPhone orientations in the embedded code.

Animation

We spent a lot of time playing around with animations and transitions in our CSS, to take advantage of the emerging support for hardware-accelerated animation in WebKit.  Unfortunately we found that much of the animation suffered from visual glitches, and in the end we had to admit defeat and turn off animation when displaying the game code within the iPhone app.  We reported all of the glitches to Apple at the time, and it’s possible they have been fixed in subsequent releases of Mobile Safari; I haven’t checked.  All kinds of things are possible with CSS and <canvas> elements (and they all looked very promising in Tim’s prototype code), but in practice the animations just weren’t quite up to the job in iPhone OS 2.0.

Hybrid development today

Since we created iDrops, the world of hybrid development has moved on considerably.  There are now several well-established hybrid app development kits available:

  • NimbleKit ($99) is a commercial framework for creating JS-based iPhone applications.
  • QuickConnect (free) takes a similar approach, enabling you to get JS-based iPhone apps up and running very quickly.
  • PhoneGap (free) is an open-source framework for creating JS-based iPhone apps for multiple mobile platforms (iPhone, Android, Blackberry and more) without needing to code separately for each platform.

QuickConnect and PhoneGap both use an undocumented UIWebView API call (webView:shouldStartLoadWithRequest:navigationType:) in order to handle JS-to-native events, which should be treated with caution given that Apple recently started testing apps for the use of undocumented APIs. However, PhoneGap state that they have had clearance from Apple for using their framework on the App Store, which is good news, and presumably goes for QuickConnect’s use of this undocumented API call too. NimbleKit apps contain an embedded web server, enabling it to handle JS-to-native events without needing to use either the undocumented API call or the didFailLoadWithError approach used in iDrops.

Apologies: the paragraph above, from my original post, contains an unfortunate error. shouldStartLoadWithRequest is very much documented and available for use. Thanks to Brian LeRoux for pointing out the mistake, and apologies for any confusion caused.  As Brian pointed out, PhoneGap is completely within Apple’s SDK licensing terms and uses only public APIs. Apologies also to PhoneGap for my error.

Each framework has its strengths, and can be really handy if you’re an experienced JS developer looking to get an iPhone app up and running quickly.  The downside is that all three of these approaches aren’t really designed for creating “write once, use anywhere” JS code for use on the web, as your JS ends up full of calls to each framework’s functions.  PhoneGap seems well set for degrading gracefully if particular features are not available on a device, but all three are designed for rapid JS-based mobile app development rather than true app-and-web code interoperability.

Summary

Creating our hybrid app was a bit of a faff at first.  There were several compromises we had to make along the way, especially given that the SDK was brand new and no-one had done the same thing before.  It was a shame not to be able to do all of the great things we wanted to do with animation, due to teething troubles with CSS animation support on the iPhone.  Moreover, some of the things we had to do (I’m looking at you, 0%-height-frameset) are particularly hacky.  Effective, but still definitely hacky.

However, once we had our development environment up and running, we were able to create a very playable and very popular game in time for the launch of the App Store.  It’s no Trism, but it still sold well.

In addition, we still had our original browser-friendly version of the game, which meant we could host a fully-playable demo of the game on our web site (compatible with Safari, IE, Firefox, Opera etc.)  Given that we still don’t have a way to release time-limited demo apps on the iPhone, this is a major benefit when selling an iPhone app.

Was it worth creating a hybrid app?  For us, definitely yes.  We could take our existing code, maintain interoperability, and produce an online demo, all without affecting the fundamental gameplay experience.  (And it enabled us to get the thing written in time for the App Store launch.)  If you have similar needs, and want to be able to sell your app on the App Store, then hybrid development may be a useful way to go.



Write a Comment

Take a moment to comment and tell us what you think. Some basic HTML is allowed for formatting.

Reader Comments

For the record: webView:shouldStartLoadWithRequest:navigationType is documented here and public here:
http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UIWebViewDelegate_Protocol/Reference/Reference.html

PhoneGap is totally within Apples SDK licensing terms and uses only public APIs.

Hi Brian,

You’re absolutely right. Many apologies. I have corrected the article above, and made a note of my error. Apologies for any offence caused.

Dave.