In part one I discussed my objective to create a "web-less webapp" which could masquerade as a native application on all modern mobile platforms. I covered the gotchas of developing with the cache, but stopped short of discussing the problems that come about when you wrap it up for each platform. In this post I'll cover the problems I encountered when wrapping the application up for iOS.
Introduction
You may remember from last time that we were looking to create a wrapper for each mobile platform such that -
Each of the wrappers would contain just enough code to show a chrome-less version of the native browser. Into that browser it would then load the webapp code, hopefully from the application cache.
A bit of research revealed that most of this work is already done for us, as all of the modern mobile platforms provide a chrome-less browser control. It's just a case of -
- Downloading the appropriate SDK
- Creating the application
- Adding a browser control to it
- Pointing the browser at the webapp
If even that seems like too much work, then you'll be glad to hear about a project called PhoneGap. It aims to maintains open source application templates for each platform which do all the above for you.
PhoneGap
From the PhoneGap download package, I installed the iOS package. Once installed a new user project template is made available in xCode called PhoneGap. Creating a new project is as simple as choosing it in the wizard and giving it a name. Aside from giving the project a name, there are a few other things you will probably want to tweak -
1. Application name
This is set to equal the target build setting "Product Name" (a.k.a. ${PRODUCT_NAME}). To modify it navigate to the target settings by expanding Targets in the Groups and Files panel and double clicking on the target (which by default will be called the project name). Go to build then search for "Product Name" and edit the value as appropriate.
2. Home screen icon
There are three icons in the Resources/icons folder which can be customised. Which icon is used depends on the device (taken from Aral Balkan's blog) -
- Icon.png (57x57 - for iPhone and iPod Touch)
- Icon@2x.png (114x114 - for iPhone 4)
- Icon-72.png (72x72 - for iPad)
3. Loading splash screen image
There are three images in the Resources/splash folder (depending on the target device) which can be customised. The Default.png image is used for the iPhone and iPod Touch, the landscape and portrait versions are used by the iPad depending on the orientation of the home screen.
4. Webapp URL
As I've chosen not to package any application code with the app, www/index.html should be changed such that the contents are as follows (obviously changing example.com as appropriate) -
<!DOCTYPE html><html><head><meta name="refresh" content="0;http://example.com"/></head></html>
There are advantages to storing some of the application code as packaged resources. An obvious one is to reduce the network loading time the first time the application is loaded. However the big disadvantage is that a new version of the application would be required in order to update the bundled resources. That means updating each of the wrappers in turn and then uploading each to each platform's app store. Personally, I prefer the idea of managing it from one location as you would any standard webapp.
With that done, we can now build and test our application on the iOS simulator that comes with the SDK. And all that without writing any ObjectiveC!
Prevent the default browser launching
Almost...it turns out that the default behaviour of the UIWebView control which is being used by PhoneGap is to launch links in a new browser window instead of to navigate to them itself. To force them to launch in place the following snippet of code needs to be added to shouldStartLoadWithRequest -
// We want the HTTP requests to stay in the app, rather than be launched in the external browser.// Analyse the request URL here and ensure we return true if we're looking at an HTTP(s) request.NSURL *url = [request URL];
if( [ [url scheme] isEqualToString:@"http"] || [ [url scheme] isEqualToString:@"https"] ) {return YES;} else {return [ super webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType ];}
Adding application cache support to an iOS application
A key factor to making this all work is making sure that we can load the application while offline using the appcache. If you read the documentation on the Apple developer center website you'd be forgiven for thinking that NSURLCache would take care of that for you. Unfortunately you'd be wrong...
For some reason that isn't well known Apple decided to remove caching to disk from iOS in such a way that it will fail silently. This has been widely reported e.g. here, here and here. Obviously without disk caching of the resources, as soon as your app is closed (or at any time that the OS sees fit to clear the memory cache) the offline support will be lost. Things are not looking good for our web-less webapp!
SDURLCache
Luckily, a comment in the last link pointed me in the direction of SDURLCache which is a project that aims to recreate the functionality of NSURLCache but without the crippled disk caching behaviour. It works because NSURLCache allows you to register a replacement cache to be used by your application.
Once you've downloaded SDURLCache and included it in your project, the following code tells your app to start using it -
SDURLCache *urlCache = [[SDURLCache alloc] initWithMemoryCapacity:1024*1024 // 1MB mem cachediskCapacity:1024*1024*5 // 5MB disk cachediskPath:[SDURLCache defaultCachePath]];[NSURLCache setSharedURLCache:urlCache];[urlCache release];
With this snippet added our application initialisation logic, we finally have offline support in our iOS app!
All wrapped up
So in the end I did have to get my hands dirty and write (copy/paste) some ObjectiveC but compared to the scale of the application, the amount of native code to enable the cache was trivial. Now we have a webapp running offline in the desktop browsers, iOS browsers and in an iOS native application. Up next Android!