Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with:

Using The Cache Manifest With iPhone's App Mode For "Native" Web Applications

Posted by Ben Nadel

After playing around with HTML5's new Cache Manifest for creating offline web applications, I thought a good next step would be to play around with the Apple iPhone's "App Mode" that allows web applications to be run more like native iPhone applications. This includes the ability to run in full-screen mode as well as use a homescreen icon and a startup "splash" screen. The technology behind these types of applications is still just HTML, CSS, and Javascript; but, with things like SQLite and the Cache Manifest, we can really start to build web applications that look, feel, and react like native iPhone applications.

To experiment with this, I took my previous Cache Manifest experiment and optimized it specifically for iPhone "App Mode" usage. Of all the changes that I made, the biggest change that I had to make to my application was to get rid of the View page. You can display an "App Mode" application using the standard Safari chrome; but, if you want to run the app in slick, full-screen mode, you necessarily have to lose Safari's navigation bar. Once the navigation bar is gone, any attempt to relocate to a new URL forces the application to break out into the standard Safari browser. As such, in order to provide the previous functionality within an App Mode context, I had to convert the demo into a very simple, single-page application that resides on a single URL.

Beyond the single-page aspect of the demo, the only additional items that I added were the iPhone-specific META and LINK tags. While I am not an expert on the META and LINK tags used for the iPhone's app mode experience, I'll try to outline them below as best as I can:

<meta name="apple-mobile-web-app-capable" content="yes" />

By default, "app mode" applications will still be rendered in the Safari browser when they are opened. This META tag, with a content value of, "Yes", tells the iPhone to run the web application in full-screen mode after it has been book marked on the homescreen (NOTE: Adding a URL to the homescreen is the only way that I know of to get the web application to run in "App Mode").

<meta name="apple-mobile-web-app-status-bar-style" content="black" />

Once you have the web application running in full-screen, app mode, you have the ability to change the style of the status bar (the very thin bar at the top of the screen). By default, the status bar is the standard light gray. This META tag allows us to change the status bar to black or black-translucent. If you use either the default gray or black, your web application displays below the status bar. If you use the black-translucent value, the web application is displayed underlapped with the translucent status bar, its contents partially showing through.

<meta name="viewport" content="width=device-width" />

By default, the view port of the iPhone in portrait mode has a resolution of 980px. Using this META tag, we can change the initial resolution of the screen to be that of a specific value or, as in my case, the width of the native device. This META tag allows other values such as Initial-Scale and User-Scalable to be defined; but, if you only set one of the values, the iPhone will infer the undefined settings as best as it can.

<link rel="apple-touch-icon" href="icon.png" />

This is a link to the 57x57 PNG or JPG icon to be used as the homescreen icon when the URL is book marked for "App Mode" usage. The iPhone will automatically apply the native-looking rounded corners and embossing to the icon for display; such effects should not be part of the original graphic.

<link rel="apple-touch-startup-image" href="home.png" />

This is a link to the 320x460 PNG or JPG graphic to be used as the startup screen when a web application is run in full-screen, app mode. To the best of my understanding, this startup screen is displayed until the initial page of the web application is entirely loaded. Of course, if you are using this in conjunction with an offline Cache Manifest, the startup load time should be rather insignificant.

So those are the META and LINK tags that I needed to add to help define the Apple iPhone "App Mode" experience. That said, let's take a look at the HTML that actually powers this web application.

  • <!---
  • Use an HTML5 doc-type. This is required for some browsers to
  • work properly with the offline-cache.
  • --->
  • <!DOCTYPE HTML>
  •  
  • <!---
  • Add a file to the cache manifest. NOTE: Any file that calls
  • a cache manifest file will be automatically cached, even if
  • it is in the WHITELIST portion of the cache manifest.
  • --->
  • <html manifest="./cache_manifest.cfm">
  • <head>
  • <title>Cache Manifest For Offline Pages</title>
  •  
  • <!---
  • By default, an iPhone web page running in App Mode will use
  • the standard Safari browser to display the content. However,
  • the following meta tag (content=yes) allows us to display web
  • apps without the standard Safari chrome (full screen mode).
  •  
  • NOTE: When you use this, you CANNOT change the URL of the
  • page. If you do, the iPhone will switch over to the standard
  • Safari app to allow for standard web page navigation.
  • --->
  • <meta
  • name="apple-mobile-web-app-capable"
  • content="yes"
  • />
  •  
  • <!---
  • Once we hide the standard Safari chrome, we can also affect
  • the way the status bar displays at the very top. By default,
  • it's that light gray color, but we can change it to be black
  • or translucent.
  • --->
  • <meta
  • name="apple-mobile-web-app-status-bar-style"
  • content="black"
  • />
  •  
  • <!---
  • By default, the "width" of the view port is 980px. This
  • gives the page a "zoomed out" feel. We can set this view port
  • initial size to an explicit size or, in my case, to the width
  • of the given device.
  • --->
  • <meta
  • name="viewport"
  • content="width=device-width"
  • />
  •  
  • <!---
  • These are the two graphics (logo and startup screen) use
  • for "app mode" on the iPhone (when the web page is put on
  • the main screen as its own logo).
  •  
  • NOTE: This part has nothing to do with the cache manifest
  • stuff - this is strictly related to runnin a website in
  • "App Mode" on an iPhone. Feel free to remove this entirely
  • from the example.
  • --->
  • <link rel="apple-touch-icon" href="icon.png" />
  • <link rel="apple-touch-startup-image" href="home.png" />
  •  
  • <!--- Include jQuery. --->
  • <script type="text/javascript" src="jquery-1.4.2.min.js"></script>
  • <script type="text/javascript">
  •  
  • // When the DOM is ready, init scripts.
  • $(function(){
  •  
  • // I handle the viewing of an image.
  • var viewImage = function( imageID ){
  • // Set the source code of the IMG tag.
  • dom.viewImage.attr(
  • "src",
  • ("./images/" + imageID + ".jpg")
  • );
  •  
  • // Hide the home page and show the view screen.
  • dom.homeScreen.hide();
  • dom.viewScreen.show();
  • };
  •  
  •  
  • // I go back to the homepage.
  • var goToHome = function(){
  • dom.homeScreen.show();
  • dom.viewScreen.hide();
  • };
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Get the DOM references that we need.
  • var dom = {
  • homeScreen: $( "#homeScreen" ),
  • pictureList: $( "#pictureList" ),
  • viewScreen: $( "#viewScreen" ),
  • backToList: $( "#backToList" ),
  • viewImage: $( "#viewImage" )
  • };
  •  
  • // Hook up the view links.
  • dom.pictureList.find( "a" ).click(
  • function( event ){
  • // Cancel the default event.
  • event.preventDefault();
  •  
  • // View the image (getting the picture ID from
  • // the "rel" attribute of the clicked link).
  • viewImage( $( this ).attr( "rel" ) );
  • }
  • );
  •  
  • // Bind the back-to-home link.
  • dom.backToList.click(
  • function( event ){
  • // Cancel the default event.
  • event.preventDefault();
  •  
  • // Navigate back to the home page.
  • goToHome();
  • }
  • );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <!-- BEGIN: Home Screen. -->
  • <div id="homeScreen">
  •  
  • <h1>
  • Cache Manifest For Offline Pages
  • </h1>
  •  
  • <h3>
  • Pictures of Alisha Morrow
  • </h3>
  •  
  • <ul id="pictureList">
  • <cfoutput>
  •  
  • <!---
  • Notice that we are dynamically generating this
  • page. It will only be generated on the first
  • request. After that, this page will be coming out
  • of the cache.
  • --->
  • <cfloop
  • index="pictureIndex"
  • from="1"
  • to="5"
  • step="1">
  •  
  • <li>
  • <a
  • href="##"
  • rel="#pictureIndex#"
  • >View Image #pictureIndex#</a>
  • </li>
  •  
  • </cfloop>
  •  
  • </cfoutput>
  • </ul>
  •  
  • <p>
  • Now:
  • <cfoutput>
  • #timeFormat( now() , "hh:mm:ss TT" )#
  • </cfoutput>
  • </p>
  •  
  • </div>
  • <!-- END: Home Screen. -->
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!-- BEGIN: View Screen. -->
  • <div id="viewScreen" style="display: none ;">
  •  
  • <h1>
  • View Image
  • </h1>
  •  
  • <p>
  • &laquo; <a id="backToList" href="#">Back to List</a>
  • </p>
  •  
  • <p>
  • <img id="viewImage" src="about:blank" width="300" />
  • </p>
  •  
  • </div>
  • <!-- END: View Screen. -->
  •  
  • </body>
  • </html>

Like I said above, since we are running in full-screen mode, I had to merge the "view" functionality into this primary page. As such, I included the jQuery library and defined some code that would allow the browser to manage linking and image display. Other than that, the only other change was to the Cache Manifest; since the "view.cfm" page was no longer being used, I was able to remove it from the list of files being cached:

Cache Manifest (cache_manifest.cfm)

  • <!---
  • Define the Cache Manifest content. I'm doing it this way since
  • the "CACHE MANIFEST" line needs to be the first line in the file
  • and storing it in a buffer allows us to TRIM later without having
  • ugly line breaks.
  • --->
  • <cfsavecontent variable="cacheManifest">
  •  
  • <!---
  • NOTE: Cache Manifest must be the very first thing in this
  • manifest file.
  • --->
  • CACHE MANIFEST
  •  
  • <!---
  • When a cache manifest is reviewed by the browser, it uses a
  • complete byte-wise comparison. As such, we can use COMMENTS
  • to defunk a previously used cache manifest. In this way, we
  • can use a version-comment to indicate change even when the
  • file list has not changed.
  •  
  • NOTE: If ANY part of this file is different from the previous
  • cache manifest, ALL of the files are re-downloaded.
  • --->
  • # Cache Manifest Version: 1.8
  •  
  • <!---
  • Let's list the file that get cached. The URLs to these files
  • are relative to the cache manifest file (or absolute).
  • --->
  • # Core files.
  • ./jquery-1.4.2.min.js
  • ./index.cfm
  •  
  • # iPhone App files (for full-screen app mode).
  • ./icon.png
  • ./home.png
  •  
  • # Images that get viewed.
  • ./images/1.jpg
  • ./images/2.jpg
  • ./images/3.jpg
  • ./images/4.jpg
  • ./images/5.jpg
  •  
  • </cfsavecontent>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Let's reset the output and set the appropriate content type.
  • It is critical that the manifest file be served up as a type
  • "text/cache-manifest" mime-type.
  •  
  • NOTE: We need to be careful about the whitespace here since
  • the very first line of the file must contain the phrase,
  • "CACHE MANIFEST". As such, we must TRIM() the content.
  • --->
  • <cfcontent
  • type="text/cache-manifest"
  • variable="#toBinary( toBase64( trim( cacheManifest ) ) )#"
  • />

And those are all of the changes that I made. Unfortunately, I don't have a good way to make a video recording of the iPhone screen. As such, you'll have to put up with a few screen shots as a visual walk through.

 
 
 
 
 
 
Running Offline Web Application In App Mode On The iPhone using Cache Manifest And META / LINK tags. 
 
 
 
 
 
 
 
 
 
Running Offline Web Application In App Mode On The iPhone using Cache Manifest And META / LINK tags. 
 
 
 
 
 
 
 
 
 
Running Offline Web Application In App Mode On The iPhone using Cache Manifest And META / LINK tags. 
 
 
 
 
 
 
 
 
 
Running Offline Web Application In App Mode On The iPhone using Cache Manifest And META / LINK tags. 
 
 
 

As you can see in these screen shots, I ran the entire web application whilst in "Airplane Mode". This means that the iPhone did not have access to the 3G network or to the WiFi system at the time. All assets required for the application to run were taken directly from the offline cache as defined in the Cache Manifest file.

The Cache Manifest, in combination with the iPhone's "App Mode" full-screen context, makes for a really beautiful team. Throw some SQLite into the mix and we'll have an "almost native" web applications on our hands. Of course, getting all three of these features to play nicely in a way that delivers a compelling user experience while, at the same time, ensuring that no data is lost... well, that's probably a lot more complicated that I am even beginning to comprehend.




Reader Comments

Back in the early days of iPhone, when developers were clamoring for a way to create apps, Apple announced "Oh you can do that now using Safari!" And everyone went "That's what we meant! That's just CGI over the Web!"

They were wrong.

You have discovered what Apple really meant when they said that. Developers were so unfamiliar with Offline Web Apps, they didn't realize what Apple was saying.

In some ways, Offline Web Apps seem like the future. But in other ways, it seems like HTML and the browser vendors were way ahead of us in knowing what we would someday want. That is, in some ways, it feels like catching up to the present.

IMHO, you want the updates to eventually be saved to a server database, the action pages of forms should never be cached. Then, if JavaScript is off, the form will post properly. (This assumes that the user has to be online, a limitation the user must accept when they turn off JavaScript.) But if JavaScript is on (the interesting case), and DOM's online flag (navigator.onLine) is false, the form's onsubmit does SQLite and returns false (to prevent an error at being unable to submit the form). That's what allows the form to be used either way, online or offline.

You can also use body.ononline() like the onScopeStart and onScopeEnd cffunctions of Application.cfc to asynchronously upload SQLite cached updates to the server.

This stuff makes me feel as excited as Flounder at the end of Animal House: "This is SOO NEAT!!"

Reply to this Comment

Ahem, I should have said "... **IF** you want updates to eventually be saved ...".

I really should proof better on sites that don't have preview.

Reply to this Comment

@Steve,

In this whole line of thinking, that is definitely where I want to be going - local data that eventually syncs to live data. In this way, I don't have to worry about the form actions being cached as I will likely be handling those via jQuery and feeding them into a local SQLite database. Of course, once I go SQLite, I'm basically locked into Safari somewhat - but if I'm building a personal, mobile app - I'm not so concerned with that (I use iPhones at this time).

I'm not yet in any real position to be thinking deeply about it at this time; miles to go before I sleep.

Reply to this Comment

Nice couple of experiments here, love the way you builld and think ypur way through a concept over time rather than just jump to "solutions"

You can actually build it a lot further easily with the jqtouch extension, like the chapter at http://building-iphone-apps.labs.oreilly.com/ch04.html

Also, if you simply up the size of the icon to 72px square, it will work for the ipad as well without any other adjustments (some jqtouch outstanding bugs on ipad so far)

...and on a sidenote the html5rocks site keeps crashin safari on the ipad f- so much for one new world united

Reply to this Comment

@Atle,

Thank for the kind words. I try to think through things step-wise so that I can really wrap my head around them. If you look at my two most recent posts on the Cache Manifest, most people would probably consider them a total waste of time (checking non-cached/cached interactivity); but for me, they really help to clear up the minutia of functionality provided by the HTML5 cache manifest.

The jQTouch project is very cool. I played around with it a bit and it seems like it was super easy to get a given kind of application up and running with a beautiful skin. I started to hit some road-blocks when I wanted to bind to the touch events used to transition from screen-to-screen. That said, whole Sencha merger (jQTouch / Ext JS / Raphael) is exciting. I'll definitely be looking into that to see what kind of power it unleashed.

Good tip about the 72x72 icon! I hadn't even thought about the iPad as being a different sized icon.

Reply to this Comment

@Atle,

That some kind of funding, I tell you what! I've downloaded the examples but have yet to really play around with it too much. Looks like something that might require a whole weekned just to really get your feet wet.

Reply to this Comment

Great article ben - i've been working with offline iphone apps for a few weeks now.

The main problem i seem to be having is when any data connection (3g/wifi/2g!) is available -- the app will try to download/check the online manifest file.

If the app is quite big - this can really delay the startup of the app (especially if the iphone starts trying to download the file over 2g).

I have tried to add a header expires to the manifest file, but so far, haven't had any luck - Has anyone managed to stop the iphone checking/downloading the online manifest - and just use the cached/downloaded one (- until the user or app makes the update)

Thanks for the help!

Reply to this Comment

@Alb,

That's a good question. From what I've read, using an expires header on the manifest itself resulted in inconsistent behavior. I vaguely remember reading about it - a guy was looking at his server logs to see what happened and different browsers seems to all act differently with the cache manifest / expires combination.

Perhaps there is a way in the Javascript to interact with the cache ready state to tell it not to check for new caching.

I'll look into the Javascript side of the cache (which I have not done yet) and see if I can find anything.

Reply to this Comment

Thanks for the great post. Unfortunately, the latest builds of mobile Safari really struggle with offline caching via the manifest. In my case, the web app that I built does not run offline at all on the iPad or iPhone. It only works offline on my desktop. Perhaps there is a disconnect between the HTML5 spec and the Safari guys, but it just doesn't work anymore.

Reply to this Comment

@Igor,

I've actually read some really shady stuff about the iOS and the "offline app" technologies. If you do a Google Search for phrases like, "ios app mode crippled," you'll see all sorts of suspicions about Apple *may* be putting hurdles in place to make sure that native apps will be more favorable than any kind of web-based app.

I really really hope that this isn't true.

But, I have to say that apps that I've tinkered with via the cache manifest don't seem to have any great caching taking place.

Reply to this Comment

@Ben,
Well, guess what: apparently when caching .DS_STORE on a Mac, it screws things up in the manifest file. As soon as I omitted that file from the cache-manifest, my application started caching correctly. The rules of the cache manifest feature are: if any file cannot be fetched from the manifest, then nothing gets cached! So .DS_STORE is a hidden file on a Mac, and therefore it was preventing everything else from being cached by the browser.
These are just my ramblings, but I deduce that this is what was happening.

Thank you,
Igor

Reply to this Comment

@Ben,
Well, guess what: apparently when caching .DS_STORE on a Mac, it screws things up in the manifest file. As soon as I omitted that file from the cache-manifest, my application started caching correctly. The rules of the cache manifest feature are: if any file cannot be fetched from the manifest, then nothing gets cached! So .DS_STORE is a hidden file on a Mac, and therefore it was preventing everything else from being cached by the browser.
These are just my ramblings, but I deduce that this is what was happening.

Thank you,
Igor

Reply to this Comment

Thanks Ben for the nice post.
I'm using Apache tomcat to host my html page and cache manifest files. I have done exactly the same you have posted in your article but its not working for me. When i'm trying to access the page in offline mode i'm getting an "Web application could not be opened because it is not connected to server " error.

Reply to this Comment

Hi,
thanks for sharing this useful blog, I start to do offline app in ipad but i encounter the limit of cache size of safari(50Mo). How To increase that, i need 1G of cache

Regards

Walid

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.