This article describes the development of a cross-platform HTML5 mobile app for searching UK property listings. The application uses JavaScript, Knockout, some elements from jQuery-Mobile and Apache Cordova (PhoneGap) for wrapping the application in order to deploy it to App-stores / marketplaces. I’ll also demonstrate how to use PhoneGap Build to create the iOS version of the application without the need for a Mac computer. The application makes use of the publically available Nestoria APIs which provide various methods for querying their database of properties for sale and rent.
This article will look at the technical details of how this application was written and the overall user-experience that this it delivers. The conclusions show that through the use of HTML5 it was possible to share quite a lot of common code between the two mobile platforms, however, the use of HTML5 did result in a few user experience compromises.
Using the above, the application would still work just fine. So why have I gone to the effort of creating these model objects? The Location
object shown below illustrates why:
Model.Location = function (config) {
this.longTitle = config.longTitle;
this.placeName = config.placeName;
};
The model objects to do not add anything to the functionality of the application, however, they make the code much more readable, providing a place for documentation and a way to specify the ‘shape’ of objects which are being passed around.
Note that the config
object allows for a more concise and readable construction of the model objects.
Here is a quick example with the use of the config
constructor:
location = new Model.Location({
longTitle: value.long_title,
placeName: value.place_name,
title: value.title
});
And the same without:
location = new Model.Location();
location.longTitle = value.long_title;
location.placeName = value.place_name;
location.title = value.title;
In summary, the model layer is really quite simple, providing a think wrapper on the Nestoria APIs.
The Property Finder application uses the same pattern I outlined in my recent MSDN Magazine Article, so I’ll go light on the details here. The MSDN article uses a much simpler example of a twitter-search application, making it easier
ApplicationViewModel
The application has a single instance of an ApplicationViewModel
, which manages the application back-stack. This view model contains an array of view model instances, with the UI being rendered (and data-bound using Knockout) for the top-most view model.
When the back-stack has more than one view model, the Cordova hardware back button event is handled, in order to capture the back button press (which would otherwise exit the application, it is a single Silverlight page after all!) and remove the top-most view model from the stack.
ViewModel.ApplicationViewModel = function () {
this.viewModelBackStack = ko.observableArray();
this.backButtonRequired = ko.computed(function () {
return this.viewModelBackStack().length > 1;
}, this);
this.currentViewModel = ko.computed (function () {
return this.viewModelBackStack()[this.viewModelBackStack().length - 1];
}, this);
this.navigateTo = function (viewModel) {
this.viewModelBackStack.push(viewModel);
};
this.back = function () {
this.viewModelBackStack.pop();
};
};
app.js
The Property Finder is structured in a similar manner to the classic Silverlight / WPF MVVM pattern, with folders for view-models, model objects etc …
The app.js file is the entry-point for the application, creating an instance of the ApplicationViewModel
and a PropertySearchViewModel
(the first page of the app), then pushing this view model onto the application back-stack.
var application,
propertySearchViewModel = null,
propertyDataSource = new Model.PropertyDataSource({
dataSource: new Model.JSONDataSource()
});
function onBackButton() {
application.back();
}
function initializeViewModel() {
application = new ViewModel.ApplicationViewModel();
application.currentViewModel.subscribe(function (viewModel) {
if (viewModel !== undefined) {
$("#app").empty();
$("#" + viewModel.template).tmpl("").appendTo("#app");
wireUpUI();
ko.applyBindings(viewModel);
var disableScroll = $(".content").hasClass("noScroll");
notifyNativeCode("scrollDisabled:" + disableScroll);
}
});
application.backButtonRequired.subscribe(function (backButtonRequired) {
if (backButtonRequired) {
document.addEventListener("backbutton", onBackButton, false);
} else {
document.removeEventListener("backbutton", onBackButton, false);
}
});
propertySearchViewModel = new ViewModel.PropertySearchViewModel();
application.navigateTo(propertySearchViewModel);
}
$(document).ready(function () {
if (window.device) {
document.addEventListener("deviceready", initializeViewModel, false);
} else {
initializeViewModel();
}
});<span style="white-space: normal; ">
</span>
You can see that the above code makes use of the ApplicationViewModel.backButtonRequired
property, which is a computed observable, which changes state between true / false, depending on whether the application needs to handle the back button.
I like to think of app.js
as roughly equivalent to the Silverlight Application instance; it handles application and page lifecycle.
PropertySearchViewModel
The PropertySearchViewModel
presents the front-page of the application, which gives the user a text field to input their search term. A snippet of this view model is shown below:
ViewModel.PropertySearchViewModel = function () {
var synchroniseSearchStrings = true,
that = this;
this.template = "propertySearchView";
this.searchDisplayString = ko.observable("");
this.userMessage = ko.observable();
this.searchLocation = undefined;
this.isSearchEnabled = ko.observable(true);
}<span style="white-space: normal; ">
</span> <span style="white-space: normal; ">
</span>
The template property of each view model is used to identify the names jQuery Template that renders its UI. The template for the PropertySearchViewModel
is shown below:
<script id="propertySearchView" type="text/x-jquery-tmpl">
<div class="content noScroll">
<h2>Property Finder UK</h2>
<p>Use the form below to search for houses to buy. You can search by place-name,
postcode, or click 'My location', to search in your current location!</p>
<div class="searchForm">
<input type="text" data-bind="value: searchDisplayString,
enable: isSearchEnabled,
valueUpdate:'afterkeydown'"/>
<button type="submit" data-bind="enable: isSearchEnabled,
click: executeSearch">Go</button>
<button data-bind="enable: isSearchEnabled,
click: searchMyLocation">My location</button>
</div>
<div class="loading" data-bind="visible: isSearchEnabled() == false">
Searching...
</div>
<p class="userMessage" data-bind="text:userMessage"/>
<div data-bind="visible: locations().length > 0">
<div>Please select a location below:</div>
<ul class="locationList" data-bind='template: { name: "locationTemplate",
foreach: locations }'/>
</div>
<div data-bind="visible: recentSearches().length > 0 && locations().length === 0">
<div data-bind="visible: isSearchEnabled">
<div>Recent searches:</div>
<ul class="locationList" data-bind='template: { name: "locationTemplate",
foreach: recentSearches }'/>
</div>
</div>
</div>
<div class="appBar">
<div class="more"><div class="pip"/><div class="pip"/><div class="pip"/></div>
<div class="icons">
<div class="icon" data-bind="click: viewFavourites">
<img src="img/favourites.png"/>
<div class="iconText">favourites</div>
</div>
<div class="icon" data-bind="click: viewAbout">
<img src="img/about.png"/>
<div class="iconText">about</div>
</div>
</div>
</div>
</script>
As you can see from the above, this is pretty straightforward Knockout stuff. There are a few points worth noting …
The content
div also has the class noScroll
, with the page lifecycle code in app.js
detecting the presence of this class. If it is found, a message is sent to the native Silverlight code to inform it that it should disable scrolling of the native WebBrowser control. This relates to a blog post I wrote a while back on how to supress pinch zoom and scroll in a Windows Phone WebBrowser in order to give a better user experience for HTML5 apps.
At the bottom of this template there is a div which creates an app-bar. The project CSS creates a Metro-style UI as shown below:
There is a small amount of code in app.js which adds various event handlers to the app-bar to give it the show / hide behaviour:
function wireUpUI() {
$(".appBar .more").click(function () {
var appBar = $(".appBar");
if (appBar.css("height") !== "80px") {
appBar.animate({ height: 80 }, { duration: 300});
}
});
$(".appBar").click(function () {
var appBar = $(".appBar");
if (appBar.css("height") === "80px") {
appBar.animate({ height: 63 }, { duration: 300});
}
});
}
The Property Finder uses a simple CSS file to achieve the Metro styling seen above. You might be wondering why I didn’t use the recently released jQuery-Mobile Metro to achieve this same result, with less effort. Unfortunately jQuery-Mobile is much more than just CSS, it adds a lot of extra structure to the DOM and has its own page lifecycle. Making jQuery Mobile play nicely with Knockout is not much fun and after my own efforts, I’d avoid attempting this!
LocationViewModel and GeoLocationViewModel
The Nestoria APIs allow you to search via a plain-text search string or a geolocation. The Property Finder has view models that represent each of these types of search.
One for geolocation based searches:
ViewModel.GeolocationViewModel = function () {
this.lat = undefined;
this.lon = undefined;
this.displayString = undefined;
this.initialise = function (lat, lon) {
this.lat = lat;
this.lon = lon;
this.displayString = lat.toFixed(2) + ", " + lon.toFixed(2);
};
this.executeSearch = function (pageNumber, callback, errorCallback) {
propertyDataSource.findPropertiesByCoordinate(this.lat, this.lon, pageNumber, callback, errorCallback);
};
};
And one for text-based searches:
ViewModel.LocationViewModel = function () {
this.searchString = undefined;
this.displayString = undefined;
this.initialise = function (searchString) {
this.searchString = searchString;
this.displayString = searchString;
};
this.initialiseDisambiguated = function (location) {
this.searchString = location.placeName;
this.displayString = location.longTitle;
};
this.executeSearch = function (pageNumber, callback, errorCallback) {
propertyDataSource.findProperties(this.searchString, pageNumber, callback, errorCallback);
};
};
Each of these view models has its own executeSearch
method, which uses the PropertyDataSource
described earlier to perform the required search. Giving the responsibility of executing the search to the objects which represent each type of search removes the need for a nasty ‘type-based’ switch to invoke the required search method.
An example of how these are used is when the user hits the ‘My location’ button, which is handled by the PropertySearchViewModel
. Here the navigation.geolocation
object, which is part of the HTML5 Geolocation specification, is used to find the current location, an instance of GeolocationViewModel
created and a search executed.
this.searchMyLocation = function () {
if (this.locationEnabled() === false) {
that.userMessage("The use of location is currently disabled. Please enable via the 'about' page.");
return;
}
function successCallback(result) {
var location = new ViewModel.GeolocationViewModel();
location.initialise(result.coords.latitude, result.coords.longitude);
synchroniseSearchStrings = false;
that.searchLocation = location;
that.searchDisplayString(location.displayString);
synchroniseSearchStrings = true;
that.executeSearch();
}
function errorCallback() {
that.userMessage("Unable to detect current location. Please ensure location is turned on in your phone settings and try again.");
}
navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
};<span style="white-space: normal; ">
</span>
The code that executes the search represented by the searchLocation instance is shown below:
this.executeSearch = function () {
that.userMessage("");
that.isSearchEnabled(false);
function errorCallback(error) {
that.userMessage("An error occurred while searching. Please check your network connection and try again.");
that.isSearchEnabled(true);
}
function successCallback(results) {
if (results.responseCode === Model.PropertySearchResponseCode.propertiesFound) {
if (results.totalResults === null) {
that.userMessage("There were no properties found for the given location.");
} else {
that.searchLocation.totalResults = results.totalResults;
that.updateRecentSearches();
var viewModel = new ViewModel.SearchResultsViewModel();
viewModel.initialize(that.searchLocation, results);
application.navigateTo(viewModel);
}
} else if (results.responseCode === Model.PropertySearchResponseCode.ambiguousLocation) {
that.locations.removeAll();
$.each(results.data, function () {
var viewModel = new ViewModel.LocationViewModel();
viewModel.initialiseDisambiguated(this);
that.locations.push(viewModel);
});
} else {
that.userMessage("The location given was not recognised.");
}
that.isSearchEnabled(true);
}
this.searchLocation.executeSearch(1, successCallback, errorCallback);
};
If you are wondering why I switch between ‘this’ and ‘that’, you probably need to read about how JavaScript handles the ‘this’ keyword, it is not the same as C#! It is common practice to assign a ‘that’ or ‘self’ variable to this within an object in order to maintain a reference to the containing object when the context changes.
SearchResultsViewModel
When a search executes successfully, the application navigates to the SearchResultsViewModel
:
ViewModel.SearchResultsViewModel = function () {
var that = this;
this.template = "searchResultsView";
this.isLoading = ko.observable(false);
this.totalResults = undefined;
this.pageNumber = ko.observable(1);
this.searchLocation = undefined;
this.properties = ko.observableArray();
this.initialize = function (searchLocation, results) {
$.each(results.data, function () {
var viewModel = new ViewModel.PropertyViewModel();
viewModel.initialize(this);
that.properties.push(viewModel);
});
that.searchLocation = searchLocation;
that.totalResults = results.totalResults;
};
this.loadMore = function() {
this.pageNumber(this.pageNumber()+1);
this.isLoading(true);
this.searchLocation.executeSearch(this.pageNumber(), function (results) {
that.isLoading(false);
$.each(results.data, function () {
var viewModel = new ViewModel.PropertyViewModel();
viewModel.initialize(this);
that.properties.push(viewModel);
});
that.pageNumber(that.pageNumber() + 1);
});
};
};
This is a simple view model that presents a collection of PropertyViewModel
instances using the template given below:
<script id="searchResultsView" type="text/x-jquery-tmpl">
<div class="content">
<div>
<div class="summary">
Search results for
<span class="searchString" data-bind="text: searchLocation.displayString"/>
, showing
<span data-bind="text: properties().length"/> of
<span data-bind="text: totalResults"/> matching properties
</div>
<ul class="propertyList" data-bind='template: { name: "propertyThumbnailView", foreach: properties }'/>
<table style="width:100%">
<tr><td>
<div class="summary">
<span data-bind="text: properties().length"/> of
<span data-bind="text: totalResults"/>
</div>
</td><td style="text-align:right">
<button data-bind="click: loadMore,
enable: isLoading() == false,
visible: properties().length!==totalResults">
Load more ...
</button>
</td></tr>
</table>
</div>
</div>
</script>
The template which renders each individual property is defined separately as follows:
<script id="propertyThumbnailView" type="text/x-jquery-tmpl">
<li class="property"
data-bind="click: select">
<div class="thumbnailContainer">
<img data-bind="attr: { src: thumbnailUrl }" class="thumbnail fade-in"/>
</div>
<ul class="propertyDetails">
<li class="price">£<span data-bind="text: price"/></li>
<li class="propertyType"><span data-bind="text: bedrooms"/> bed <span data-bind="text: propertyType"/></li>
<li class="title" data-bind="text: title"></li>
</ul>
</li>
</script>
If there are more pages of data, a ‘Load more …’ button is displayed at the end of the list:
State Persistence
Hopefully the previous sections are enough to give you a flavour of how the Property Finder application is structured and functions. I have not given an exhaustive descriptions of all the application features, such as recent searches, favourites etc … however, these all follow a similar pattern.
The one area I wanted to detail a bit further is state persistence. Within app.js
property changed handlers are added to all the view model properties that we would like to persist between application sessions:
propertySearchViewModel.favourites.subscribe(persistentStateChanged);
propertySearchViewModel.recentSearches.subscribe(persistentStateChanged);
propertySearchViewModel.locationEnabled.subscribe(persistentStateChanged);
When the state changes, a JSON representation of these various objects is saved to local storage:
function persistentStateChanged() {
var state = {
recentSearches : propertySearchViewModel.recentSearches,
favourites: propertySearchViewModel.favourites,
locationEnabled : propertySearchViewModel.locationEnabled
},
jsonState = ko.toJSON(state);
localStorage.setItem("state", jsonState);
}
Cordova does its magic here, replacing the localStorage
object with its own equivalent that provides a platform specific mechanism for saving state, i.e. for Windows Phone it uses isolated storage via the Silverlight APIs.
When the application restarts, we check for any previously saved state and re-load it:
function initializeViewModel() {
application = new ViewModel.ApplicationViewModel();
propertySearchViewModel = new ViewModel.PropertySearchViewModel();
application.navigateTo(propertySearchViewModel);
try {
var state = localStorage.getItem("state");
console.log("loading state:" + state);
if (typeof (state) === 'string') {
setState(state);
}
} catch (err) {
}
}
function setState(jsonState) {
var state = $.parseJSON(jsonState);
if (!state) {
return;
}
if (state.favourites) {
$.each(state.favourites, function () {
propertySearchViewModel.favourites.push(hydrateObject(this));
});
}
if (state.recentSearches) {
$.each(state.recentSearches, function () {
propertySearchViewModel.recentSearches.push(hydrateObject(this));
});
}
if (state.locationEnabled !== undefined) {
propertySearchViewModel.locationEnabled(state.locationEnabled);
}
}
The saved application state is in JSON format, for we can easily re-create our view model objects, using the ko.fromJSON
utility function for example. However, this will provide objects that look like our view models, but they will lack the methods we have added to these objects within their constructor function.
For this reason I have created a utility function, hydrateObject
, that recursively re-constructs view models, where each has its constructor function identified by a factoryName
property:
function hydrateObject(state) {
if (!(state instanceof Object)) {
return state;
}
var property, unwrapped, propertyValue,
viewModel = new window["ViewModel"][state.factoryName]();
for (property in state) {
if (property === "template" ||
property === "factoryName" ||
property === undefined) {
continue;
}
propertyValue = state[property];
if (viewModel[property] instanceof Function) {
unwrapped = ko.utils.unwrapObservable(viewModel[property]);
if (viewModel[property] !== unwrapped) {
if (unwrapped instanceof Array) {
$.each(propertyValue, function () {
viewModel[property].push(hydrateObject(this));
});
} else {
viewModel[property](propertyValue);
}
}
} else {
viewModel[property] = hydrateObject(propertyValue);
}
}
return viewModel;
}
Because Property Finder has been written using platform-agnostics HTML5, it could be run directly on an iPhone. However, because I decided to use the Windows Phone Metro style, it would look very odd on an Apple device! Instead, I wanted to give users of both OS an experience that is suited to their device; Metro for Windows Phone and the classic ‘Apple’ theme for iOS.
All of the application logic is written using Knockout view models, so is entirely separate from the UI layer. This means it should be possible to replace the Metro UI with an iOS equivalent simply by changing the templates and style sheets.
Well … almost.
Borrowing from jQuery Mobile
Creating an iOS style UI using HTML / CSS is a much harder task than creating a Metro UI with HTML. Fortunately the
jQuery Mobile team have come up with a highly comprehensive framework that produces HTML UIs that look almost exactly the same as their native equivalents, with minimal effort.
Unfortunately, as I mentioned previously, Knockout and jQuery Mobile do not play nice! So I used the jQuery Mobile CSS without their JavaScript code. This has the side-effect that my HTML templates are much more complex.
What was previously a simple button:
<button type="submit" data-bind="enable: isSearchEnabled, click: executeSearch">Go</button>
Now becomes this monstrosity:
<div class="ui-btn ui-btn-inline ui-btn-corner-all ui-shadow ui-btn-up-c">
<span class="ui-btn-inner ui-btn-corner-all">
<span class="ui-btn-text">Go</span>
</span>
<input class="ui-btn-hidden" type="button" value="Go"
data-bind="enable: isSearchEnabled,
click: executeSearch"/>
</div>
The extra HTML elements are required in order to support the jQuery Mobile CSS (If only HTML / CSS had the equivalent of Silverlight templates!).
Using this slightly verbose approach, I was able to create HTML templates that make use of the jQuery Mobile CSS resulting in iOS screens which look very much like a native application:
Scrolling with iScroll
The ‘standard’ layout for an iPhone application has a fixed header bar at the top and scrolling content beneath. Unfortunately iOS browser (prior to iOS5) lack so of the CSS constructs required to achieve this type of page layout. For an overview of the issues, refer to the jQuery Mobile page on touchOverflow.
In order to create a page with a fixed header and scrolling content, people resort to some quite complex JavaScript and CSS, manually handling touch events, offsetting the content, calculating inertia etc … A popular script for that wraps up all of the complex code required is iScroll. You can see a live demo of it in action here. In order to render a scrolling list of properties, I integrated iScroll into the iOS version of Property Finder.
There is a little extra code required in app.js
in order to handle changes to the list of properties and update the iScroll instance so that it is aware that its contents have changed. Other than that, the integration was quite straightforward:
Again, the list layout uses jQuery Mobile CSS and hand-crafted HTML to match.
The iPhone lacks a hardware back button, hence the inclusion of a back button within the header bar in the above screenshot.
iScroll also has some very nice extra features, such as pull-to-refresh. I was able to integrate this feature into PropertyFinder as an alternative to the ‘Load more …’ button in the Windows Phone version:
The code which handles this is a bit messy, as it is outside of the elegant Knockout view model-driven code:
function wireUpUI($view) {
fadeImages();
var $iScroll = $view.find(".iScrollWrapper"),
$pullUpEl = $view.find(".pullUp"),
$pullUpLabel, pullUpEl, pullUpOffset;
if ($iScroll.length > 0) {
if ($pullUpEl.length > 0) {
pullUpEl = $pullUpEl.get(0);
pullUpOffset = $pullUpEl.attr('offsetHeight');
$pullUpLabel = $pullUpEl.find(".pullUpLabel");
myScroll = new iScroll($iScroll.get(0), {
useTransition: true,
onRefresh: function () {
if ($pullUpEl.hasClass('loading')) {
$pullUpEl.removeClass();
$pullUpLabel.html('Pull up to load more...');
}
},
onScrollMove: function () {
if (this.y < (this.maxScrollY - 5) && !$pullUpEl.hasClass('flip')) {
$pullUpEl.addClass('flip');
$pullUpLabel.html('Release to refresh...');
this.maxScrollY = this.maxScrollY;
} else if (this.y > (this.maxScrollY + 5) && $pullUpEl.hasClass('flip')) {
$pullUpEl.removeClass("flip");
$pullUpLabel.html('Pull up to load more...');
this.maxScrollY = pullUpOffset;
}
},
onScrollEnd: function () {
if ($pullUpEl.hasClass('flip')) {
$pullUpEl.addClass("loading");
$pullUpLabel.html('Loading...');
pullUpAction();
}
}
});
} else {
myScroll = new iScroll($iScroll.get(0), {
useTransition: true
});
}
}
}
It should be possible to create a version of iScroll which has proper binding support, the Knockout framework is quite easy to extend to add custom bindings … but that’s a job for another day!
The fadeImages
function adds a cool little effect to the image thumbnails, fading them in as they load. This is achieved using a CSS3 opacity transition:
img.ui-li-thumb
{
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-webkit-backface-visibility: hidden;
opacity: 0;
}
img.shown
{
opacity: 1;
}
Which is triggered by adding the shown
class to images when they have loaded:
function fadeImages() {
$("img.ui-li-thumb:not(.shown)").bind("load", function () {
$(this).addClass("shown");
});
}
The Project Structure and Build
Other than the cosmetic changes detailed above, all of the core application functionality, including geolocation and state persistence, work s unchanged on iOS. In order to facilitate the development of the iOS version, I created a simple batch file which copies the shared code from the Windows Phone project into my iOS folders:
copy ..\HTML5PropertySearch\www\viewModel\*.js viewModel /Y
copy ..\HTML5PropertySearch\www\model\*.js model /Y
copy ..\HTML5PropertySearch\www\lib\*.js lib /Y
The iOS version is not built with Visual Studio, so I cannot use VS file references!
In order to deploy the application to an iPhone, it needs to be Cordova wrapped and packaged as an IPA file. This can be done on a Mac computer using Xcode, however there is a simpler alternative.
You can build your Cordova application online via the PhoneGap Build service. For Property Finder, the application assets are accompanied with this XML file:
="1.0"="utf-8"
<widget xmlns = "http://www.w3.org/ns/widgets"
xmlns:gap = "http://phonegap.com/ns/1.0"
id = "uk.co.scottlogic.propertyfinder"
version = "1.0.0">
<name>Property Finder</name>
<description>
A HTML5 property finder application
</description>
<author href="http://www.scottlogic.co.uk/blog/colin/"
email="ceberhardt@scottlogic.co.uk">
Colin Eberhardt
</author>
<gap:platforms>
<gap:platform name="android" minVersion="2.1" />
<gap:platform name="webos" />
<gap:platform name="symbian.wrt" />
<gap:platform name="blackberry" project="widgets"/>
</gap:platforms>
<icon src="ApplicationIcon.png" gap:role="default" />
<gap:splash src="SplashScreenImage.png"/>
<preference name="orientation" value="portrait" />
<preference name="webviewbounce" value="false" />
<feature name="http://api.phonegap.com/1.0/geolocation"/>
<feature name="http://api.phonegap.com/1.0/network"/>
</widget><span style="white-space: normal; ">
</span>
On uploading the code, plus this configuration file, the Build service builds your code of a range of devices. Here is what the online portal looks like:
As you can see, I have used this service for quite a few projects. My friend Chris Price has also created a useful Maven plugin that automates the process of sending your code to PhoneGap Build, allowing continuous integration.
The static images in this article look almost indistinguishable from their native contemporaries. However, when you actually put them in the hands of an end user, they will start to spot a few of the tell-tale signs that identify these as non-native applications!
Windows Phone
Windows Phone HTML5-based applications have a few issues that affect the user-experience of the finished product. Here is a brief summary of some of those issues:
- Text selection – because the PhoneGap view is HTML, the user can select any text on the page. With WebKit browsers (iOS, Android), you can disable this with the CSS user-select:none property. This is not supported in IE, which is a shame because it improves the user experience of HTML-based applications.
- Suppression of pinch zoom – the solution I described above for disabling pinch and tap zoom works well, however, as it is dependent on the internal visual tree of the
WebBrowser
control. For this reason, when I discussed it with Jesse Macfeyden of the Cordova team, I advised against including it being included in the Windows Phone Cordova framework. A future version of the WebBrowser
control might have a different visual tree that breaks this functionality. What is really needed is better support for the user-scalable:no meta property. Both Android and iOS do a better job of this!
- Gray link highlighting – probably the single biggest issue with the IE9 mobile browser, from a HTML5 application developer perspective, is the way it highlights links, or any element with a JavaScript click event handler, in gray. If you are trying to create an application with a native look and feel it totally ruins the experience. Try looking at the jQuery Mobile demo on your Windows Phone browser. It is an almost pixel perfect iOS UI, however as soon as you click on a link, it is immediately obvious that this is a web page.
In the screenshot below you can see this issue, where a gray rectangle appears over the property tile when the user clicks on it:
This occurs throughout the application, when they search buttons, app-bar buttons, tiles, everywhere! Regarding the last issue, I have posted on StackOverflow but haven’t found a satisfactory solution yet.
iPhone
The Property Finder iPhone interface holds up a little better than the Windows Phone equivalent. There are no immediately obvious UI flaws that identify it as a HTML5 application. However, there are a few signs. Probably the most noticeable difference is the page transitions. With native iOS applications, the transition from one screen to the next is subtle and complex, involving at least five different elements.
HTML5-based applications typically transition from one screen to the next as a single sliding block.
The main reason for choosing to implement mobile applications with HTML5 is in order to share code across multiple platforms, removing the need to create multiple native equivalents. So just how much code was shared between the iOS and Windows Phone Property Finder?
Using lines of code as a metric, 43% of the code was shared across platforms:
This doesn’t sound that impressive! However, it is worth noting that this is over the entire codebase, which includes JavaScript, CSS and HTML. In terms of development effort, it typically takes much less time to write CSS and HTML than JavaScript.
If we focus on JavaScript alone, the re-use story is much better:
Another reason why the amount of code shared was not as high as it could have been is that I decided to create an application that mimicked the native look and feel of each of the two platforms. Nearly all of the platforms specific code was as a result of this decision.
The following is a brief summary of my findings, in an easily digestible bullet-point format:
- HTML5 is a viable technology for cross-platform mobile application development. Both the Windows Phone and iOS version delivered the required end user functionality.
- HTML5 does allow you to share code across platforms. All of the core business logic was shared across both platforms. This is a key benefit of the cross-platform approach, where you can write and test your logic just once.
- HTML5 is a route of compromise. While both applications look ‘at home’ within their respective platforms, it is quite easy to spot the fact that they are not native. The Windows Phone version has a few glaring UI quirks that do spoil the user experience. Personally, I would not choose HTML5 for Windows Phone development at the moment, perhaps Windows Phone 8 will solve these issues? For iOS the differences are more subtle, but are still noticeable.
- Matching the native look and feel is costly. It did take quite a bit of time to match the native iOS UI. There are frameworks that assist with this, such as jQuery Mobile, however, these are often not suitable for more complex applications and you find yourself fighting the framework.
- If you want a premium experience, go native! I don’t think it is possible to create a HTML5 application that matches the ‘premium’ experience of a native equivalent. If you do not want to compromise … native is your only option.
My advice to anyone who is considering creating a HTML5-based cross platform mobile application is to ignore the iOS style, ignore the Windows Phone Metro and Android Roboto and create your own application style that can be shared across all platforms. Creating a single consistent UI style will significantly reduce your development costs. And finally, understand the compromises you will have to make.
I hope you have enjoyed this article … my next one is already underway, where I will look at using Xamarin / MonoTouch as an alternative to HTML5.
You can download the full sourcecode for this article, or download / fork the codebase on github.