Room112 – PhoneGap Exploration

Whenever I start to play around with a new technology, platform or framework I usually plan out a small and easy to complete project that tries to address some of features I am most interested in testing out. To vet out PhoneGap I decided to build a simple app called Room112 to keep track of my hotel room number when I travel. This exploration document outlines how I approached architecting my app but also some of the issues I ran into while exploring how PhoneGap works.

Room112 is a simple way to keep track of your hotel room number. Enter your hotel room number into the app and the next time you open it the number is displayed for you along with the time you checked in, the floor your room is on and a map of the hotel’s location. TripIt doesn’t really track the last two bits of information. This app is just like the card a hotel would give you with your room number on it.

Goals

I collaborated with Wesley Crozier (who did all the artwork) on Room112 with the goal of creating a simple PhoneGap app that looks and feels as close to native as possible. I travel a lot and always forget my hotel address, room number and floor. I set the following goals when creating this application:

  • Create a simple one-screen app
  • Test our local storage
  • Make the app feel like a native iOS app
  • Have most design done via CSS3
  • Test out animation on mobile Web browsers
  • Take advantage of some simple jQuery
  • Understand how to set up and publish a PhoneGap app in Xcode
  • Display a Google Maps image via built-in GPS support

The full source code for this project is availbile on github here.

Architecture

Room112 is very simple. When you launch the app it checks for saved room data. If no data exists you are taken to the check-in screen. If data does exist then the check-in card is displayed with the saved data. I also planned on being able to review past check-in data, but this feature didn’t make it into the initial release.

Over the next few pages, we will discuss each of the application's screens based on the designs with functional notes on the logic.

Loading Screen/Background

This represents the loading screen of the application, which is the same as the background image.


Loading Screen

When the application is fully loaded, a small pocket is faded in to allow for easier masking of the check-in card animation.


Pocket Image


Background Plus Pocket

Once the application is fully loaded, the two layers (background and pocket) work very well to mask off the card animations and offer up a little more depth to the design.

Before Check-In

This is the screen you see after launching the app when no hotel information is saved. Simply enter a room number to activate the Check In button and save your data.


Before Check-in

During this process, the user is not able to actually check in until the room is entered. All other data is optional to make adding room data as fast as possible. Once a room number is added, the Check In button is highlighted and the user can save their data.

Check In

When you have entered your hotel information you are ready to check in. The Check In button becomes active, allowing the data to be saved.


Checked In

One thing to note is that the date is automatically filled in before checking in. Again, the goal is to simplify the check-in process by not forcing the user to have to add information we can assume is going to be contextual to the check-in process. Also, the user can add their room number as well as a map to the hotel’s location by pressing on the top part of the card at any time.

Once the user checks in, the card slides down into the pocket and the Check In button is changed to a Check Out button indicating that the data is set. At this point, if the user exits the app and comes back, their data will be saved and re-displayed until they check out.

Check Out

After you have checked in, you can edit your data at any time or check out. Checking out clears the card data and a new empty card is shown allowing the user to start the process over.


Check Out

Once the user hits the Check Out button, they are asked if they are sure they want to proceed.

If the user gives the OK to check out, the card slides out of the pocket and off the top of the screen and is reset before a new card slides back down into view.

Error Screen

There is only one error state for the app, which is displayed when the app is unable to locate the GPS position of the check-in.


Map Error Screen

Error handling and messaging is done by swapping out images. The initial state of the application is to display the Add Your Location image. If there is a failure, the Could Not Locate Your Position image is displayed. If the location can be used, an image of the map from Google is loaded in on top of the display area.


Error Screen Details

Code Review

Due to the nature of this application, the codebase was architected to be as small and portable as possible. I did not break out the JS logic into classes and relied heavily on frameworks to help speed up my development. Let’s take a look at what is going on under the hood.

Application Structure

For the most part the application is very flat. There is a standard www directory, which houses all the Web content loaded by PhoneGap. In that directory are my css, images, js and index.html files.

In addition to the www folder, PhoneGap automatically adds all the needed libraries to run on iOS when you create a new PhoneGap template from Xcode.

It’s important to note that once you create a PhoneGap project you will need to manually link the www directory into the project. Here is how the project looks in the finder:

Notice the www sites outside of the Xcode Room112 project and how I tend to include my own design directory in the root to keep my source designs organized, especially when I check in a project.

Let’s take a quick look at each of the directories in the www folder.

  • css - There are two css files, one for the base of the project and the other that has overrides for retina resolution graphics. I’ll talk more about this in the next section.
  • images - This contains all the images the Web side of the PhoneGap application requires to display.
  • js - This contains all my JavaScript libraries. I am using the following libraries for this project:
    • jquery-1.7.1.min.js - to handle animation, changing data on the page, etc.
    • jquery.animate-enhanced.min.js - allows jQuery animations to use CSS3 transitions where supported to help improve performance on mobile
    • lawnchair-0.6.1.min.js - handles local storage
    • phonegap-1.3.0.js - PhoneGap’s APIs and native to JS bridge
    • room112.js - application logic

Room112 Core Code

I designed the application to be a simple state machine. It handles three states: not checked in, checked in and checking out. From there I was able to manage the state changes and update the display accordingly. Here is a high-level overview of the app’s functions and what they do:

  • onDeviceReady - This is the main init function of the application. It handles setting up the initial visuals of the application along with requested saved room information from local storage via Lawnchair. Once everything is set up, the room card is animated in and the app is ready to be used.
  • loadGPS - This handles making the request to the browser’s built-in GPS API:
  • navigator.geolocation.getCurrentPosition(showMap, showMapError);

    I decided to use the built-in browser’s GPS instead of the API provided by PhoneGap to help minimize any dependencies on their library. The call to navigator.geolocation accepts two callbacks, one for success and the other for failure. When the request is made, a pop-up is shown automatically to allow the user to accept permission to use the built-in GPS.

  • showMap- This is called when the GPS is able to find the user’s location. It simply makes a request to Google Maps for an image and displays it at the top of the check-in card. The URL for the map is saved locally so the service doesn’t have to be called when the app re-loads and the user is checked in.
  • cacheMapURL = "http://maps.google.com/maps/api/staticmap?center=" + coords.latitude + "," + coords.longitude + "&zoom=13&size="+width+"x"+height+"&maptype=roadmap&key=MyGoogleMapsAPIKey&sensor=true";
    

    The method accepts a position argument that can be used to determine the coordinates the Google Maps API needs to generate the image. I also provide the image size manually so I can support standard resolution and retna.

  • showMapError - This is called if the GPS is unable to return a valid position to the application. I simply show the error image allowing users to try again later.
  • addInputListeners - This handles adding listeners for the buttons and other UI elements such as each input field. This allows me to listen for when an input field loses focus so I can call invalidate() in order to see what data has changed.
  • invalidate - This method compares the data in the form to what has been saved. This allows the application to automatically save any changes made while checked in and also test if the user has entered enough information in order to check in.
  • updateDisplay - This manages updating the action button based on the state of the application.

  • Before Check-in State


    Able to Check In


    Check Out

  • getHotelData - This is a simple utility method for accessing saved hotel data from Lawnchair.
  • formAction - This is called after invalidate() and simply switches between the application’s mode methods: checkIn(), checkOut() and saveRoomData().
  • checkIn - This is the default state of the application when no saved data is found upon launch.
  • checkOut - This animates the card off the screen, calls resetCard() to clear the data and resets the application’s mode method back to checkIn().
  • saveRoomData - This is called when the user is checked in and they change any values in the form.
  • resetCard - This clears the data on the check-in card while it is off-screen. This is called after the card is animated off the screen.

Design/Retina Display Support

The application was designed with iPhone 4S Retina display support in mind. In order to do this you must provide two resolution images and a secondary CSS sheet to override the base image paths. This is relatively easy to set up.

<link rel="stylesheet" type="text/css" href="css/normal.css">
<link rel="stylesheet" type="text/css" href="css/retina.css" media="only screen and (-webkit-min-device-pixel-ratio: 2)">

As you can see, the initial CSS sheet is loaded and then we use a media query, which is supported on WebKit on iOS to detect if the pixel ratio is 2x larger. Here is an example of a low res image:

#pocket
{
background: url('../images/pocket.png') no-repeat;
background-size: 320px 80px;
width: 320px;
height: 80px;
position: absolute;
bottom: 0px;
}

and the retina override:

#pocket
{
background-image: url('../images/pocket@2x.png');
}

It’s important to note that in the base style we need to define the background size so that when the retina image loads it’s not rendered at its native resolution. Here are the two images:


pocket.png 320 x 80px


pocket@2x.png 640 x 160px

All the images in the application have retina size graphics to support the higher resolution, and the difference visually on the device is noticeable. On a website, this approach would be problematic since we are loading both sets of images and would probably want to use a better solution so the application is only loading the right image to cut down on bandwidth. I didn’t take this into account since it was a locally running app, but if the app is image intensive this could end up being a performance issue.

Finally, there is some mobile WebKit-specific CSS code you should add to your style sheets as well to help make your app look and feel more native.

<!--
* {
-webkit-touch-callout: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-text-size-adjust: none;
-webkit-user-select: none;
}
-->

This allows you to hide the fact that your app is running in WebKit and remove highlight colors on taps and user selection on form elements. Also, adding the following metadata block will disable scaling and zooming, which is built into the WebView by default:

<meta name="viewport" content="width=device-width; initial-scale=1.0; user-scalable=0;">

This extra line of CSS and meta tag will make your app run at native resolution and is important to have in your project’s HTML code.

Animation

I chose to go with jQuery for the animation and ran into performance issues, which I will discuss later on. My reasoning for this was to make sure the app wasn’t dependant on CSS3-specific animations that may not be supported on other platforms. To me this was a big concern even though I focused on an iOS app only, so if you have a clear understanding of where you application will be deployed to you may just want to go with CSS3 animations from the beginning.

jQuery did allow me some additional control with call backs and delays, which I still don’t find to be well supported with CSS3 animation right now, especially when you need to execute JS during a key point in the animation sequence.

Local Storage

I chose to go with a local storage wrapper framework called Lawnchair. Using local storage directly is relatively easy, but I had read good things about it and liked its implementation. It is very straightforward to set up a pointer to local storage by doing the following:

hotelData = new Lawnchair({name:'hoteldata'}, function (e) {console.log('Storage Open', this); });

Then, at any point when I need to save data, I simply reference the Lawnchair instance and save my data out.

var formData = {
room:$("#room").val(),
floor:$("#floor").val(),
date:$("#date").val(),
map: cacheMapURL
}
hotelData.save({key:"room", value:formData});

I save out my application’s state to a simple JSON object, which makes loading and parsing the data very easy.

Development Workflow

Browser Prototyping

I experimented with several workflows, the first of which was doing all my development locally and testing it out in Safari on the Mac. This allowed me to quickly prototype the application using the tools I was most comfortable with along with WebKit’s built-in debugger. One of the biggest issues I ran into while testing was being able to simply debug my CSS and JS, which is incredibly difficult to do when running the PhoneGap application on the iPhone simulator.

Development in Xcode

Once I was happy with the application and the way it ran in Safari, I moved the project over to Xcode. Development tended to slow down considerably for me since debugging issues and JS integration were difficult without better debug tools. The big downside to testing in Xcode is the compile time. While it isn’t too bad, it does take a few seconds to go back and forth between the two environments.

I did find Xcode to have good JS support and was even able to continue to use Xcode to write and fix my JS logic without much issue.

Deployment

Once you have everything set up in a PhoneGap project, your application runs just like any other iOS application. Technically, this is a native app, so you get to take advantage of Xcode’s built-in support for setting up provisioning.

Issues

I ran into several issues that in the end made it hard for me to make Room112 feel like a native app. I have outlined a few of the issues below to help you address them in your own app as well as things to keep in mind when planning out your own development.

Animation Performance

I ran into lots of issues trying to get native-looking animation using PhoneGap. At first my issue was the performance penalty of using jQuery for animation. Doing direct DOM-based animation is incredibly slow. There are lots of hacks you can do in order to force hardware-accelerated CSS3 animation on iOS, and it wasn’t until I started using jquery.animate-enhanced that I began seeing better performance. Also, at some point everything slowed down on later builds and I was unable to debug.

My suggestion for iOS is to simply use CSS3 transitions from the beginning and make sure you take advantage of the -webkit-transform: translate3d(0,0,0); hack, which is said to force all other CSS3 translations to be hardware-accelerated. This could also be problematic if you are trying to directly port over an existing mobile site to PhoneGap, which may not have support on other platforms for CSS3 transitions. I will probably continue to experiment with this to find the best solution with good fallback support to allow the code to be as portable as possible.

PhoneGap Issues

I was running into a lot of issues trying to disable the native scrolling of the Web page via JS. The only way I was able to fully disable it was to add the following Objective-C code to disable scrolling and bouncing directly in the PhoneGap WebView instance:

UIScrollView* scroll;
for(UIView* theWebSubView in self.webView.subviews){
if([theWebSubView isKindOfClass:[UIScrollView class] ]){
scroll = (UIScrollView*) theWebSubView;
scroll.scrollEnabled = false;
scroll.bounces = false;
}
}

I ended up having to add this to the AppDelegate.m class, which is auto-generated by PhoneGap because the JS code to ignore scroll events didn’t block the user from scrolling from inside a text field. This is a good example of how you may need to augment your PhoneGap app with native code when JS hacks simply don’t work.

I was also running into issues trying to access the Google Maps APIs and had to whitelist the URL in PhoneGap’s .plist file, which is located in the native project.


Project -> Support Files -> PhoneGap.plist

Once you have PhoneGap.plist open, simply go to the ExternalHosts property and add any URLs your application will need access to.

After that you shouldn’t have any issues.

Alerts and Messages

I ran into several strange issues with displaying alerts in my app. At first I was trying to avoid using the native PhoneGap hooks so I could continue to test my app in Safari on my computer, but when you call JS’s default alert you’ll see the parent page the alert is being called into.

I wasn’t really expecting this, and it certainly takes away from the native feel of the app. I tried to switch over to PhoneGap’s built-in native alert message bridge but had problems getting it to run. I was using an older version of PhoneGap, so hopefully updating it will fix the issue. Other people on the forums had similar issues with it as well.

Graphics Issues

I didn’t run into many issues, but the few that I did see are slightly annoying. The biggest issue I noticed is after the application loads there is a split second flash of white, which happens when the WebView loads right after the loading image is removed and the index.html page’s background hasn’t fully loaded. I believe there are ways to keep the loader image up longer to smoothe out the transition, but that would have to be done in Obj-C and there isn’t much you can do about it. This just means you need to pay special attention to the loading of your main page, especially if it is graphically intensive or requires a lot of JS to initialize.

The only other issue I want to highlight was the difficulty I ran into with getting fonts to match up exactly with my Photoshop designs. At first I tried to do the entire design via CSS3 and use fonts where possible. I was unable to get the layout to match my PSD to my satisfaction and ended up using rasterized images instead for most of the UI. While this app would be relatively easy to localize, since I could swap out different images via CSS based on the default language of the phone, in a production application this would not be ideal. While CSS3 and browser fonts have come a long way, there is still a gap between what can be done in the browser versus Photoshop, so that limitation should be in the back of your mind when trying to figure out how to set up the visuals of your PhoneGap app.

Development Bottlenecks

While I only focused on doing an iOS application, I have heard of issues building for Android. In addition, it became next to impossible to maintain a Web-only version of the app. So, while it would be ideal to be able to deploy the same code base to a mobile website and have the PhoneGap wrapper app for people who want to use native, you will end up having to modify each build for each platform.

You could write a wrapper to emulate or disable the PhoneGap-specific JS logic so that your application would degrade better on the Web, but the level of work required to architect a single app that could run on the Web and in PhoneGap on all its supported platforms would be a huge undertaking. It’s not impossible but I just don’t think that it ends up being worth the level of effort. In that case you are better off defining blocks of code that are portable and spending the last 10-20% of your development customizing the code base to each platform.

Moving Forward

While this was a fun little exercise to help me get up and running in PhoneGap and vet out Web-based app building, I still have a lot to do and learn.

Deploying to Multiple OSs

The next steps are going to be getting this app to run on different OSs such as Android and Windows Phone 7. The biggest reservation I have with porting the app to other platforms is that each one has its own UI and navigation paradigm. While Room112 was designed according to what most iOS apps are starting to look like, using real-world textures to look photo realistic, this may look out of place on Android and Windows Phone 7. Luckily, I could easily swap out the CSS and images and quickly reskin the app; it’s just extra work and platform-specific customization I don’t want to do. This is just another problem with the dream of having a single app that runs and uses the same code base across multiple platforms.

iPad/Tablet Support

I would also like to eventually support the iPad, tablets and other higher-resolution devices but would have to rethink my design approach. Right now the app doesn’t warrant a larger-resolution design, but I would like to test out how to handle scaling up in a PhoneGap app. I may just try this on another project. Again, I hope it would be a few quick CSS changes, but most phone apps need to be rethought out for tablets so it would be interesting to see how much of the application logic I could keep and what the performance would be on larger screen devices.

Conclusion

While this wasn’t the most awe inspiring application I have ever built, it did give me a great opportunity to learn more about PhoneGap as well as how to style for iOS mobile browsers. All in all, while PhoneGap is easy to use and being able to leverage a Web Stack for development is a great advantage, it’s also incredibly difficult to get a native feel and responsiveness most users would be expecting on iOS.

I think if you approach your PhoneGap app understanding the limitations and pain points, you can actually produce something really polished and portable across multiple platforms. I wouldn’t rule this out because over time, as devices get faster and support more HTML5 features, this may turn out to be a viable development solution.

Subscribe To My Mailing List

Want to learn how to make a game? Not sure where to start? Even if you are a seasoned game maker there is still a lot you can learn from my free 15 page guide on how to build, release and market a game.

Simply sign up for my mailing list and also get access to great tips and advice on how to make better games. I promise to not spam your inbox!

Join Now