“Almost Native” – iOS Apps Powered By iAd.js and PhoneGap

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:

<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>
  • Eduardo

    The impression I got at the start of the article is that it would explain and demonstrate the importance of multi-platform development as a “solution accelerator”. However, the example is clearly not multi-platform: it just runs on iOS. Why is this better than packaging an Air application for iOS?

    • http://www.tricedesigns.com Andrew

      Eduardo, This example demonstrates a solution accelerator for iOS applications. Developing the same content in Obj-C would be more complex, would require a higher degree of skill, and would likely require more time for implementation (This example took less than a day). This example is not intended to be multi platform (as stated in the post), rather a faster way to develop for iOS. This is an option for mobile development, not necessarily compared to AIR for “which is better”.

      Thanks,
      Andrew Trice

  • Mike Butcher

    Hi Andy,
    I am trying to get the app to run. I downloaded iAd.js and iAd.css. I am getting movie json data from RottenTomatoes.

    I see a table with movie titles but when I click on the movie I get an error :

    imageView.image = iAd.Image.imageForURL( item.posters.profile );

    Can I get the zipped project?
    Thanks
    Mike

    • http://www.tricedesigns.com Andrew

      What is the error message you are getting? That line of code is javascript, but doesn’t indicate what the issue might be? You should also be able to run this example in chrome with the security policy disabled, and can use the webkit debugging tools to see what is happening at runtime. Sorry, I can’t share the full app b/c it includes iad.js, which would violate Apple’s agreement. My code is exactly as its shown in the blog, I do not have any other changes. The only thing I can think may be an issue is if there have been changes to iAd.js since I wrote that post.

  • Mike Butcher

    I am using iAD.js
    /* iAd JS Version: 1.3 – r1873 */

    I was using Chrome with AJAX enabled

    the error is:
    Uncaught TypeError: Object function (){b.processed||iAd.Class.processAllClasses();b.prototype.init.apply(this,arguments)} has no method ‘imageForURL’