mikeash.com: just this guy, you know?

Posted at 2009-12-04 19:29 | RSS feed (Full text feed) | Blog Index
Next article: Friday Q&A 2009-12-11: A GCD Case Study: Building an HTTP Server
Previous article: Friday Q&A 2009-11-27: Using Accessors in Init and Dealloc
Tags: html iphone javascript web
Friday Q&A 2009-12-04: Building Standalone iPhone Web Apps
by Mike Ash  

Welcome to another edition of Friday Q&A. This week I'm going to talk about building standalone iPhone web apps, web apps that have an icon on the home screen, and which start a separate program when tapped, just like native apps, a topic suggested by Mike Shields.

iPhone web apps have been in the news a fair bit lately as a way to bypass Apple's troublesome review process. While web apps aren't as capable as native apps, and almost certainly never will be, they're still interesting to work with simply because they're so much simpler to develop and deploy.

Neven Mrgan's Pie Guy is perhaps the most prominent example. It's a complete Pac-Man look-alike built as a standalone web app, albeit one which, because it's all HTML and JavaScript, requires a 3GS to run smoothly.

While the ability to build apps like this is well known, I haven't seen anything that gathers all the requisite parts in one place and walks through how to build one, so that's my intent today. I cribbed much of this information from dissecting how Pie Guy does it, and don't think for a moment that I discovered any of this stuff myself.

Getting Started
In this post I'll walk through the process of creating a basic standalone web app. My example app simply queries the JavaScript location object and displays your latitude and longitude. You can try the completed app, or get its source with Subversion:

    svn co http://mikeash.com/svn/PhoneWebApp
The scope of this post is simply building the parts that are special to iPhone standalone web apps. The actual HTML and JavaScript for the app functionality itself is beyond the scope of the discussion, but there are of course a wide range of resources out there for them.

Separate Program The first thing that you want in a standalone web app is for it to start as its own program, rather than loading into Safari, when the user taps your icon on the home screen. To make this happen, you simply add a tag to your setting apple-mobile-web-app-capable to yes, like so:

    <meta name="apple-mobile-web-app-capable" content="yes">
While you're in there, it can be useful to set the viewport of your web page to achieve 1x zoom instead of the default (which is around ⅓x zoom), and to disallow the user from changing the zoom factor. This helps make your web page act more like a real app. You can do this by adding another tag:
    <meta name="viewport"
          content="width=device-width; height=device-height; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;">

Icons and Startup Images
Another thing that's a must for real iPhone apps is to have an actual icon and a startup image that's displayed while the app is loading. You can specify an icon by referencing it in a tag with apple-touch-icon-precomposed set as the relationship, and you can specify a startup image with apple-touch-startup-image:

    <link rel="apple-touch-icon-precomposed" href="icon.png">
    <link rel="apple-touch-startup-image" href="default.png">
The images themselves need to be 57x57 for the icon and 320x460 for the startup image.


Putting the above together, and with the page title and link to the app's JavaScript code, the entire tag looks like this:

    <head>
        <title>PhoneWebApp</title>
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="viewport" content="width=device-width; height=device-height; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;">
        
        <link rel="apple-touch-icon-precomposed" href="icon.png">
        <link rel="apple-touch-startup-image" href="default.png">
        
        <script src="main.js" type="text/javascript" />
    </head>

Body
The body of the page is, of course, where you put all the stuff for your web app to interact with the user. Beyond the basic functionality of your app, you'll also want some iPhone-specific sections. You'll want to display a different page to users who view the app on a non-iPhone browser, to tell them to reload it with an iPhone. You'll also want to display a different page to users who view the app on an iPhone, but who have not yet installed it, to tell them how to install it.

For this particular app, I also have three more pages. One is a "loading" page, which is visible on initial load and gives the computer something to display until the JavaScript kicks in. One is a page to display in case navigation services aren't available for some reason. And finally, I hae the real page that displays the navigation data.

Each conceptual "page" is actually a

. All but the initial "loading" page are set to be hidden, so that they can then be selectively un-hidden from JavaScript after things load.

With all of that, here's what the tag looks like:

    <body onload="load()">
        <div id="loading" style="position: absolute; left: 0; top: 0;">
            Loading....
        </div>
        
        <div id="noiphone" style="visibility: hidden; position: absolute; left: 0; top: 0;">
            This is an iPhone web app. Load this page on an iPhone!
        </div>
        
        <div id="notinstalled" style="visibility: hidden; position: absolute; left: 0; top: 0;">
            To install this web app, tap the + button at the bottom of the page, then select "Add to Home Screen".
        </div>
        
        <div id="nonavigation" style="visibility: hidden; position: absolute; left: 0; top: 0;">
            Location services aren't available for some reason.
        </div>
        
        <div id="navigation" style="visibility: hidden; position: absolute; left: 0; top: 0;">
            Longitude: <span id="longitude">N/A</span>
            <br>
            Latitude: <span id="latitude">N/A</span>
        </div>
    </body>

Scripting
With the HTML out of the way, it's time to start scripting. The tag references a load() function which will get called once the page is loaded. The first thing it does is hide the "loading" page:

    function load()
    {
        document.getElementById("loading").style.visibility = "hidden";
Then it has to decide which of the other four pages to display. First, it checks for a non-iPhone browser:
        // no iPhone
        if(navigator.appVersion.indexOf('iPhone OS ') < 0)
        {
            document.getElementById("noiphone").style.visibility = "visible";
        }
Next, it checks to see if it's on an iPhone but not running standalone:
        else if(!window.navigator.standalone)
        {
            document.getElementById("notinstalled").style.visibility = "visible";
        }
Then a check to see if navigation services are available:
        else if(!hasNavigation())
        {
            document.getElementById("nonavigation").style.visibility = "visible";
        }
If all of these checks fail, then the app is running standalone on an iPhone and navigation services are available, so it can start doing normal app tasks:
        else
        {
            // we're on a phone, and installed standalone
            // "real" app init code goes here
            document.getElementById("navigation").style.visibility = "visible";
            
            navigator.geolocation.watchPosition(positionWatcher);
        }
    }
For the above code to work, it needs a function to check for the availability of navigation services, which is easy:
    function hasNavigation()
    {
        return (typeof navigator.geolocation != "undefined");
    }
And another function to respond to position updates by updating the GUI:
    function positionWatcher(location)
    {
        document.getElementById("longitude").textContent = location.coords.longitude;
        document.getElementById("latitude").textContent = location.coords.latitude;
    }
That's it!

Caching
The above code will work great to build a simple little web app that acts like a native app, with one significant exception: native apps work even when you have no data service, and this does not.

Even this problem can be solved, though. Specify a manifest file in the tag at the top of the HTML file:

    <html manifest="cache.manifest">
The manifest file itself must start with a line saying CACHE MANIFEST, and then subsequent lines refer to the files that the app needs to access, one per line. The manifest file for this app looks like this:
    CACHE MANIFEST
    
    index.html
    main.js
    default.png
    icon.png
One more trick: the manifest file must be served with a content type of text/cache-manifest, otherwise iPhone Safari won't recognize it. The Subversion repository includes a .htaccess file which declares that MIME type, but beware that if you're trying to test it with OS X's personal web sharing, you have to do some configuration hacking to enable .htaccess support, which is beyond the scope of this post.

With the manifest in place, the iPhone will cache all resources locally when the user adds the bookmark, and the app will launch and function even when no data access is available. (Try enabling Airplane Mode to test it.)

While developing the app, the cache manifest can be really annoying, so it's a good idea to remove it from the tag while you're working on it. Apple says that the browser will reload everything if there are any changes in the manifest file (like adding a blank line), but while working on this example I found this to be frustratingly unreliable.

Conclusion
iPhone web apps don't behave as well as native apps, and probably never will, but with these few simple tips you can bridge the gap much more closely than with just a simple web page, and create an app that completely bypass the App Store. Building an interesting and useful app with HTML and JavaScript once you're in is, of course, up to you!

For further reading, Apple's Safari Web Content Guide documents all of the special tags I used here, and has a lot of other useful information as well. Also, I linked to this at the beginning but it merits a second mention, Neven Mrgan's Pie Guy is a great example of these tehcniques that you can examine to see how it ticks on the inside.

That's it for this week. Come back in another seven days for the next scarifying edition. Friday Q&A is driven by user ideas, so if you have an idea you'd like to see covered here, send them in!

Did you enjoy this article? I'm selling whole books full of them! Volumes II and III are now out! They're available as ePub, PDF, print, and on iBooks and Kindle. Click here for more information.

Comments:

Packaging an HTML/JS webkit-based app in to an actual, installable app which can be sold through the app store is also possible using PhoneGap:

http://www.phonegap.com/

A pretty neat piece of technology; their philosophy is to take advantage of the common web languages that most smart phones can interpret through their web browsers in order to make device agnostic, cross compatible apps.
To me, the only reason to ever use JavaScript/HTML to build an app is to bypass the App Store. Something like PhoneGap seems pointless to me, as you then get the worst of both worlds: you get the poor development environment of the web, and you also get Apple's abusive policies.
Hey Mike. I've been doing iPhone web app development for clients for a while. Neven's Pie Guy inspired me to start producing some apps of my own as standalones. My first one is available from http://taptacular.com

It's pretty boring, just a mortgage calculator, but viewing the source will show a few techniques that are improvements on Pie Guy.

E.g., there's no need to use JavaScript to detect things like whether the user is on an iPhone/iPod Touch, nor which orientation the phone is being held in. These can both be done using CSS @media queries. (I do use JavaScript to detect standalone mode so I can show an alert to users to nudge them towards installing the app as a proper icon on their home screen.)

I have a couple of games in development as well that use hardware accelerated animations as well. These should do more to show of what WebKit is capable of.

Anyway, great article! I'm glad to see these techniques gain a higher profile. Back in the day, when Apple declared that web apps were the iPhone SDK, developers groaned, but WebKit is finally becoming a viable platform for a lot of apps. The last pieces of the puzzle are going to be tighter integration with device APIs such as accelerometer, camera, compass, etc. Once Safari exposes that functionality, there will be even less need to build apps using Cocoa Touch.
Andrew Hedges wrote:

> Once Safari exposes that functionality, there will be even less need to build apps using Cocoa Touch.

I had thought this was an open question, not a certainty. Has that changed of late?
Technically, yes, it's an open question. I think it's just a matter of time before it happens because there are sure to be efforts on other platforms (e.g., Android) to expose this functionality through the browser. Apple will eventually either lead the effort or find themselves playing catch-up.
I downloaded your files from svn but couldn't make the page load in Firefox until I changed the JS include to use a normal end tag < /script >

I believe the < script src type / > is xhtml, right?

Sorry about the extra spaces in the tags - I added them to make sure that the tags weren't being interpreted.

/Mogens

Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

Name:
The Answer to the Ultimate Question of Life, the Universe, and Everything?
Comment:
Formatting: <i> <b> <blockquote> <code>.
NOTE: Due to an increase in spam, URLs are forbidden! Please provide search terms or fragment your URLs so they don't look like URLs.
Code syntax highlighting thanks to Pygments.
Hosted at DigitalOcean.