One of the benefits of using a cross-platform development technology that I mentioned in my last post is that cross-platform development technologies can be solution accelerator platforms. I mean “solution accelerator” because it can be easier and faster to develop an application using cross-platform technologies (web development skills) rather than native development, even if you are only targeting a single platform. This isn’t the case for every solution, but certainly can be for many scenarios.
Here’s a sample application that I put together while exploring the PhoneGap product offering. For those that weren’t aware, Adobe has entered an agreement to acquire Nitobi, the makers of PhoneGap, and PhoneGap is an HTML5 app platform that allows you to author native applications with web technologies and get access to APIs and app stores.
This application consumes data from the rottentomatoes.com API, and is built entirely in JavaScript. If you haven’t seen it before, RottenTomatoes.com is a site for searching movie ratings & information. Check out the video below, and then we’ll examine how the application was built (be sure to check out the CoverFlow runtime performance at 34 seconds).
The entire codebase that I had to write for this application is 289 lines of HTML & JavaScript (including whitespace), all in a single file, and was built using iAd.js, xui.js, and of course, PhoneGap. The first thing you might be wondering is “what is iAd.js?” or, if you know about the iAd platform, you might be thinking “um… isn’t that just for advertisements?”. Everyone who uses an iOS device has probably encountered an iAd at least once, whether they know it or not. The iAd program is an advertising platform for iOS devices that enables advertisers to create rich, engaging, & interactive advertisements. Interestingly, all iAds are built on top of a JavaScript framework built by Apple.
It just so happens that the iAd.js JavaScript framework can also be used outside of the advertising context (only on iOS devices – I tried Android as well, but not all of the elements worked correctly). Not all features of the iAd framework will work outside of the advertising context, such as purchasing or interacting with the iTunes store. However the iAd.js framework will provide you with user interface elements that look nearly identical to native iOS components. This includes view navigators, table views (with groups, disclosure indicators, etc…), carousel views, cover flow views, wheel pickers, progress bars, sliders, switches, buttons, and much more. In addition, the interactions and animations for these components are highly optimized for mobile safari, and interaction with these elements feels very, very close to native. There are a few minor things here and there, but overall it is not necessarily easy to distinguish the difference.
Note: I have not submitted any apps using this technique to Apple’s app store. However, I have heard from others that Apple has accepted their applications which are built using this approach.
While these components are instantiated in the browser and created via HTML & JavaScript, the programming model of iAd components is very similar to native iOS development. You still have the usage of protocols (interfaces), and controller & delegate patterns. For example, using the iAd.TableView ui component, still requires use of the TableViewDataSource and TableViewDelegate protocols (just implemented in JavaScript). Familiarity with native iOS development will definitely be a big plus if you are using the iAd.js framework.
I used xui.js to simplify the syntax for XMLHttpRequest for asynch data loading, and of course, PhoneGap is used for the application container, as well as any native OS interaction if you wanted any. The source code could certainly be broken up into multiple controllers or separate files for maintainability, however I was just going for a “quick & dirty” example.
Basically, you just need to include the iAd JavaScript and CSS files, then build your application as you would any other HTML/JS PhoneGap experience. You can download the iAd framework from here if you are a registered Apple iOS developer. In theory, this isn’t that much different from using jQuery mobile components, however these have better performance on iOS, and have a more-native feel.
Thanks to pixelranger and merhl from Universal Mind for showing me a while back that you could use this approach!
Full source code below:
[as3]
<html>
<head>
<title>RottenTomatoes</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<!– Import iAd Assets –>
<link rel="stylesheet" href="iAd/iAd.css">
<script type="text/javascript" src="iAd/iAd.js" charset="utf-8"></script>
<!– Import other libraries –>
<script type="text/javascript" charset="utf-8" src="libs/xui-2.2.0.js"></script>
<script type="text/javascript" charset="utf-8" src="libs/phonegap-1.1.0.js"></script>
<style type="text/css" media="screen">
body {
margin: 0;
overflow: hidden;
}
.ad-flow-view {
position: relative;
left: 0px;
width: 300px;
height: 380px;
-webkit-perspective: 400;
}
.ad-flow-view .ad-flow-view-camera {
position: absolute;
left: 50%;
width: 10px;
height: 10px;
}
.ad-flow-view .ad-flow-view-cell {
position: absolute;
top: 30px;
left: -125px;
width: 250px;
height: 350px;
}
.ad-flow-view .ad-flow-view-cell img {
pointer-events: none;
width: 180px;
height: 267px;
-webkit-box-reflect: below 0px -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(0.8, transparent), to(rgba(255,255,255,0.35)));
}
</style>
<script type="text/javascript" charset="utf-8">
var API_KEY = "put your api key here";
var sampleData;
/* ==================== Controller ==================== */
var controller = {
data : []
};
controller.init = function () {
var url = "http://api.rottentomatoes.com/api/public/v1.0/lists/movies/box_office.json?limit=15&country=us&apikey=" + API_KEY;
console.log( url );
x$().xhr( url, {
async: true,
callback: function() {
var trimmedResponse = this.responseText.replace(/^\s\s*/, ”).replace(/\s\s*$/, ”);
if ( trimmedResponse.length > 0 )
{
sampleData = eval( "(" + trimmedResponse + ")" );
if ( sampleData )
{
controller.data = sampleData.movies ;
controller.table.reloadData();
}
}
}
});
this.navigation = new iAd.NavigationController();
this.navigation.delegate = this;
iAd.RootView.sharedRoot.addSubview(this.navigation.view);
this.navigation.navigationBar.barStyle = iAd.NavigationBar.STYLE_BLACK;
this.navigation.pushViewControllerAnimated(this.createTableViewController(), false);
};
controller.handleEvent = function (event) {
if ( event.type == iAd.View.TOUCH_UP_INSIDE_EVENT ) {
var viewController = this.createFlowViewController();
this.navigation.pushViewControllerAnimated(viewController, true);
viewController.view.hidden = false;
}
};
controller.displayDetails = function (index) {
var item = this.data[index];
var viewController = this.createDetailViewController(item);
this.navigation.pushViewControllerAnimated(viewController, true);
viewController.view.hidden = false;
}
controller.createTableViewController = function (index) {
if ( this.viewController == null ) {
this.viewController = new iAd.ViewController();
this.viewController.title = "Rotten Tomatoes";
this.viewController.navigationItem.rightBarButtonItem = new iAd.BarButtonItem();
this.viewController.navigationItem.rightBarButtonItem.style = iAd.BarButtonItem.STYLE_DONE;
this.viewController.navigationItem.rightBarButtonItem.title = ‘Flow’;
this.viewController.navigationItem.rightBarButtonItem.addEventListener( iAd.View.TOUCH_UP_INSIDE_EVENT, this, false );
// create a TableView
this.table = new iAd.TableView();
this.table.tableStyle = iAd.TableView.STYLE_GROUPED;
this.table.delegate = this;
this.table.dataSource = this;
this.table.size = new iAd.Size(window.innerWidth, window.innerHeight-46);
this.viewController.view.addSubview(this.table);
}
return this.viewController;
};
controller.createFlowViewController = function (index) {
var viewController = new iAd.ViewController();
viewController.title = "Flow";
var flowView = new iAd.FlowView();
flowView.dataSource = this;
flowView.delegate = this;
flowView.layer.style.backgroundColor = "#FFFFFF";
// customize flow view
flowView.sidePadding = 150;
flowView.cellRotation = 65;
flowView.cellGap = 50;
flowView.dragMultiplier = 1.0;
flowView.sideZOffset = -200;
// load the data
flowView.reloadData();
flowView.centerCamera();
this.flowView = flowView;
viewController.view.addSubview(flowView);
viewController.view.hidden = true;
this.flowSelectedIndex = 0;
return viewController;
};
controller.flowViewNumberOfCells = function(flowView) {
return this.data.length;
};
controller.flowViewCellAtIndex = function(flowView, index) {
var cell = document.createElement(‘div’);
cell.appendChild(document.createElement(‘img’)).src = this.data[index].posters.detailed;
return cell;
};
/* ==================== iAd.FlowViewDelegate Protocol ==================== */
controller.flowViewDidTapFrontCell = function (flowView, index) {
console.log(‘flowViewDidTapFrontCell ‘ + index + ", " + this.flowSelectedIndex);
if ( this.flowSelectedIndex == index )
this.displayDetails( index );
};
controller.flowViewDidSelectCell = function (flowView, index) {
console.log(‘flowViewDidSelectCell ‘ + index);
this.flowSelectedIndex = index;
};
controller.flowViewDidBeginSwipe = function (flowView) {
console.log(‘flowViewDidBeginSwipe’);
};
controller.flowViewDidEndSwipe = function (flowView) {
console.log(‘flowViewDidEndSwipe’);
};
controller.createDetailViewController = function (item) {
var viewController = new iAd.ViewController();
viewController.title = item.title;
var scrollView = new iAd.ScrollView();
scrollView.userInteractionEnabled = true;
scrollView.size = new iAd.Size(window.innerWidth, window.innerHeight-46);
scrollView.horizontalScrollEnabled = false;
scrollView.layer.style.backgroundColor = "#FFFFFF";
var imageView = scrollView.addSubview(new iAd.ImageView());
imageView.image = iAd.Image.imageForURL( item.posters.profile );
imageView.position = new iAd.Point(10, 10);
imageView.size = new iAd.Size(120, 178);
var synopsisLabel = scrollView.addSubview(new iAd.Label());
synopsisLabel.text = item.synopsis;
synopsisLabel.numberOfLines = 0;
synopsisLabel.size = new iAd.Size(this.navigation.view.size.width-150, 800 );
synopsisLabel.position = new iAd.Point(140, 10);
synopsisLabel.autoresizingMask = iAd.View.AUTORESIZING_FLEXIBLE_WIDTH | iAd.View.AUTORESIZING_FLEXIBLE_HEIGHT;
synopsisLabel.verticalAlignment = iAd.Label.VERTICAL_ALIGNMENT_TOP;
viewController.view.addSubview( scrollView );
viewController.view.hidden = true;
return viewController;
};
/* ==================== TableViewDataSource Protocol ==================== */
controller.numberOfSectionsInTableView = function (tableView) {
return 1;
};
controller.tableViewNumberOfRowsInSection = function (tableView, section) {
return this.data.length;
};
controller.tableViewCellForRowAtPath = function (tableView, path) {
var cell = new iAd.TableViewCell();
cell.text = this.data[path.row].title;
cell.detailedText = ‘title’;
cell.accessoryType = iAd.TableViewCell.ACCESSORY_DISCLOSURE_INDICATOR;
cell.selectionStyle = iAd.TableViewCell.SELECTION_STYLE_BLUE;
return cell;
};
controller.tableViewTitleForHeaderInSection = function (tableView, section) {
return "Box Office Movies";
};
controller.tableViewTitleForFooterInSection = function (tableView, section) {
return "Powered by RottenTomatoes.com";
};
/* ==================== TableViewDelegate Protocol ==================== */
controller.tableViewDidSelectRowAtPath = function (theTableView, path) {
this.displayDetails(path.row);
};
controller.tableViewDidSelectAccessoryForRowAtPath = function (theTableView, path) {
};
/* ==================== iAd.NavigationViewDelegate Protocol ==================== */
controller.navigationControllerWillShowViewControllerAnimated = function (theNavigationController, viewController, animated) {
};
controller.navigationControllerDidShowViewControllerAnimated = function (theNavigationController, viewController, animated) {
};
/* ==================== Init ==================== */
function init () {
console.log( "init" );
controller.init();
}
window.addEventListener(‘load’, init, false);
</script>
</head>
<body></body>
</html>[/as3]