Screencasts

Creating iPad Apps with JavaScript using Titanium

Download Files ↓

Show Notes

In a follow up to our popular Building iPhone Apps with JavaScript using Titanium screencast we show you how to create an iPad app without learning a line of Objective-C!

Given the screen real estate of the iPad we just don't want a blown up clone of the iPhone app. We want to take full advantage of the screen real estate and the specific UI elements that the iPad has to offer.

Links

What You'll Learn

  • How to use the iPad's Split View in Titanium
  • How to use WebView in Titanium
  • How to detect if the iOS device is online and on Wi-Fi network with Titanium.Network
  • How to create animations

Update - 30 May 2011

Blip.tv have changed their API so we've pushed an update to the App Store.

To fix the code yourself, all you need to do is change:

...
function getVideoData(jsonData){
  return jsonData[0];
};
...

To:

...
function getVideoData(jsonData){
  return jsonData[0][0];
};
...

Update - 21 June 2011

Blip.tv have changed their API again. To fix use the following code:

...
function getVideoData(jsonData){
  return jsonData[0].Post;
};
...

Script

Welcome to the Creating iPad Apps with JavaScript using Titanium screencast. We'll be modifying and building off the iPhone project we created in our previous screencast, titled Building iPhone Apps with JavaScript using Titanium. So we suggest you first watch that screencast to get up to speed on how to install and setup Titanium, and how to build many of the basic app features we'll be using today.

Quick Note

In our first iPhone Titanium screencast, we mentioned that we were submitting that app to the App Store. We wanted to let you know that we had to add in some extra code in order to get the app approved. Since our app played videos longer than 10 minutes over a cell network, Apple required the use HTTP Live, and we needed to include a baseline 64 kbps audio-only HTTP Live stream. Since we don't currently have the resources to do that, we decided to include a check for a Wi-Fi connection, and made video playback Wi-Fi-only. We'll show you how to do this in this screencast.

The Split-View Layout

The iPad's larger screen real estate allows for a wider range of possible navigation structures and layouts. The most iPad-specific layout is called the Split-View layout, which, in landscape orientation, has a master window to the left, and a detail window to the right. In portrait orientation, the detail window fills the screen, and the skinny master window becomes a popover window linked off a button in the top left of the navigation bar. The guideline is that the master window acts as the app's main navigation, while the detail window displays majority of the content. Apple's Mail app for the iPad is a good example of a split-view app, with the list of messages in the master window, and message content in the detail window.

Create our project

Let's create our new project in Titanium, and select iPad from the project type menu. Then we'll fill in the rest of the app details and hit Create Project.

Add Supporting Files

To follow along with this screencast, you'll need to download several assets including images and support functions. They're available to download in a zip file on Screencasts.org. Be sure to download and extract these into the Resources folder. We've included an app.js file which has some code we'll reuse from our iPhone project.

Now that the project has been created, let's open the project folder in our text editor of choice.

Includes

At the top of our app.js file, above the iPhone project code, let's include 3 files which we'll discuss in more detail shortly. We'll use the Ti.include command to do this.

Ti.include('getLighterColor.js');
Ti.include('addEmptyRows.js');
Ti.include('colorCycle.js');
...

Check internet connection

First in our app, let's create a function called notConnectedAlert to display when we're not connected to the internet. This will create an alertDialog with a title and message that tells the user they aren't connected to the internet. We have one button in the alert named Continue. Then, we show the alertDialog.

Right outside this function, let's do an initial check to see if we're online when the app loads, by checking the Boolean Ti.Network.online. If false, we'll call ournotConnectedAlert` function. We'll be placing checks like this throughout the app whenever we need to get data from the internet.

...
function notConnectedAlert(){
  var alertDialog = Titanium.UI.createAlertDialog({
    title: 'Not connected to the internet',
    message: 'Sorry, but Screencasts could not connect to the internet, and could not access data.',
    buttonNames: ['Continue']
  });
  alertDialog.show();
}

if(!Ti.Network.online){
  notConnectedAlert();
}
...

Our Screencasts iPad App

Our iPhone app was structured around a tab group. We had a 'Latest' tab and a 'Topics' tab. The topics table view drilled down to a list of episodes for each topic, and whenever an episode row was tapped, it played the corresponding screencast. We'd like to keep all of that functionality in our iPad app, but we'd also like to do something with the extra screen real estate that we couldn't do with the iPhone. So we'll be reusing some code from the iPhone project, but only the code that creates our table views and navigation. We'll be writing new code to playback the videos.

Let's create a window called tabGroupWindow, which will contain the tabGroup code used in our previous iPhone project. The reason we need to do this is because the masterView cannot accept a tabGroup object as its root. It requires a window for it to behave properly.

...
var tabGroupWindow = Ti.UI.createWindow();
...

Now that we've done that, we're going to create a new navigationGroup that will represent the detail side of our app. We'll first create a detailWindow, setting its title to Welcome, and then we'll create a detailNav with the createNavigationGroup command, setting its backgroundImage to grid.png, and its window to our detailWindow.

...
var detailWindow = Ti.UI.createWindow({
  title:'Welcome'
});

var detailNav = Ti.UI.iPhone.createNavigationGroup({
  backgroundImage:'grid.png',
  window: detailWindow
});
...

Creating a navigationGroup for the detail side gives us the option of pushing on and popping off any windows onto the detail area.

Now we'll create the actual split view, assigning our detailNav as the detailView, and the tabGroupWindow as the masterView.

We'll open the split view in just a second, after all the tabGroup code from the previous iPhone project. Just remember it's not enough to just create objects like this in Titanium, you have to open them.

...
var splitWindow = Ti.UI.iPad.createSplitWindow({
  detailView: detailNav,
  masterView: tabGroupWindow
});
...

We'll also add an event listener for when visible, which will add the button at the top left of the navigation bar when in portrait mode, but null it out when in landscape.

...
splitWindow.addEventListener('visible', function(e){
  if(e.view == 'detail')
  {
    e.button.title = "Menu";
    detailWindow.leftNavButton = e.button;
  } else if (e.view == 'master')
  {
    detailWindow.leftNavButton = null;
  }
});
...

Master View

Now, we have the code we copied in from our previous iPhone project. We need to make a quick edit to where we open the tabGroup in this code. Before we open the tabGroup, we need to add the tabGroup to our tabGroupWindow. Then after we open the tabGroup, we're going to open our splitWindow.

...
var tabGroup = Titanium.UI.createTabGroup();
tabGroup.addTab(latestTab);
tabGroup.addTab(topicsTab);
tabGroupWindow.add(tabGroup);
tabGroup.open();
splitWindow.open();
...

If we launch the app now in Titanium, we'll see that our split view layout is displaying correctly. If we rotate the simulator by pressing command + the left or right arrows, we'll see the two layout variations. And if we click through our tabs and table views in the masterView, we'll see that they're all functioning as expected. Great.

Detail View

With all of that extra space in the detail view, we thought it would be nice to not only display the video here, but also the show notes and script. So we decided to have a video player in the top of the detail view, and then a scrollable web view below it, which will contain each episode's show notes and script.

We'll also create a nice little welcome page for the detail view when the app first loads.

So, let's create the webView for the show notes and script by calling Titanium.UI.createWebView. For now, we're not going to pass in any parameters. We'll just add it to the detailWindow.

...
var webView = Titanium.UI.createWebView();
detailWindow.add(webView);

Welcome page

Next, we're going to create the 'Welcome' view that will appear in the detailView when the app loads. We're going to have a little bit of fun with this, and create a Screencasts.org logo with the beam cycling through different colors.

So we'll create an image view named screencastsImage, with its image parameter set to our image path. We'll also set the backgroundColor to a hex value of a red (#C52F24), and then set some positioning parameters. Setting the bottom to zero and right to zero aligns the view to the bottom right corner. We're also setting the width and height here to the actual dimensions of the image. Setting a view's size like this, along with the bottom-right positioning, is our way of positioning the image so that it looks good in both portrait and landscape orientations. If you don't set these variables, Titanium will adjust your images so they scale-to-fit, which we don't want in this case. Our screencastsImage has transparency where the 'beam' should be, so the red backgroundColor will show through for the bar color.

...
var screencastsImage = Titanium.UI.createImageView({
  image:'screencasts-home-ipad.png',
  backgroundColor:'#C52F24',
  bottom:0,
  right:0,
  width:768,
  height:960
});

var screencastsImageIsLoaded = true;

We'll then add the screencastsImage to our detailWindow, and set a boolean value that we'll call screencastsImageIsLoaded, to true. We'll come back to this in a minute.

Now we want to animate the background color that shows through in the screencasts beam from red, to orange, to blue, and back again to red. We'll call animate on our screencastsImage, passing in an animation we've called animation1. We've put the code for this animation into a support file called colorCycle.js, which we've included at the top of app.js.

...
detailWindow.add(screencastsImage);
screencastsImage.animate(animation1);

If you look in colorCycle.js, you'll see that we create 3 animations with Titanium.UI.createAnimation, setting duration to 5000, which is 5 seconds, and backgroundColor to hex values for an orange, a blue and a red respectively. The backgroundColor we set here is the destination for this animation. You could also set other variables to animate, such as position, size, opacity, and so on. You can also set a repeat count, whether or not the animation should reverse, the animation's "curve", and many other parameters. See Appcelerator's documentation for more information on animations.

var animation1 = Titanium.UI.createAnimation({ duration:5000, backgroundColor:'#dd9b00' });
var animation2 = Titanium.UI.createAnimation({ duration:5000, backgroundColor:'#1169ae' });
var animation3 = Titanium.UI.createAnimation({ duration:5000, backgroundColor:'#C52F24' });

Next, we addEventListeners for each of the animations, so that on complete, animation1 calls animation2, animation2 calls animation3, and animation3 calls animation1. So we've essentially created a little loop of animations that will continue to cycle through these colors.

animation1.addEventListener('complete',function() { screencastsImage.animate(animation2); });
animation2.addEventListener('complete',function() { screencastsImage.animate(animation3); });
animation3.addEventListener('complete',function() { screencastsImage.animate(animation1); });

If we launch the app again in Titanium, we'll see that our new welcome page is displaying correctly, and our color cycle animation is working and repeating properly. Great.

Loading Videos

Let's move on to the videos. Our video loading code is going to be very similar to what we wrote in our iPhone app. We're going to create a VideoPlayer using a URL we'll get from blip.tv. Again, we'll be using an HTTPClient to get the video data from blip.tv.

So we'll create a videoPlayer with the createVideoPlayer command. Then, we'll create a videoView with Ti.UI.createView, setting its width to the detailWindow width, and height to the correct height for our video aspect ratio, which is 16 by 9. Then, we add the videoPlayer to our videoView.

We then set the top for our webView to the height value for our video. This way, our webView window with show notes and the script will fit below the video.

...
var videoPlayer = Ti.Media.createVideoPlayer();
var videoView = Ti.UI.createView({
  width: detailWindow.width,
  height: (detailWindow.width / (16/9)),
  top: 0
});

videoView.add(videoPlayer);
webView.top = videoView.height;

Activity Indicator

In many instances, Titanium provides built-in activity indicators for things that are loading. But in this case, we're going to need to create our own. We'll use the createActivityIndicator command. We'll then add it to our videoView, but it won't become visible until we later call the show method.

...
var activityIndicator = Titanium.UI.createActivityIndicator();
videoView.add(activityIndicator);

loadVideoXhr

Now, we'll create a loadVideoXhr HTTPClient object, then create its onload function. We'll assign the variable called blipTVjson to the JSON response we get by evaluating this.responseText with the eval function.

The blip.tv API offers an array of additionalMedia. These are the possible video formats that are available to us. Each media value in the array has a url and a role. We want to find the role Blip HD 720, and store the url for that media. We'll do this by cycling through the blipTVjson.additionalMedia array, looking for the role that matches Blip HD 720. When we find it, we set our videoPlayer.url to that role's url.

...
var loadVideoXhr = Ti.Network.createHTTPClient();
loadVideoXhr.onload = function() {
  var blipTVjson = eval(this.responseText);
  
  for (var i=0; i < blipTVjson.additionalMedia.length; i++) {
    if(blipTVjson.additionalMedia[i].role == "Blip HD 720") {
      videoPlayer.url = blipTVjson.additionalMedia[i].url;
    }
  };
}

After that loop, we're going to show the activityIndicator. Then we'll add an event listener to the videoPlayer for when the video is playing. In this anonymous function, we'll tell the activityIndicator to hide. So this activity indicator will only show between the time we set a new video URL, and when the video actually starts playing.

...
var loadVideoXhr = Ti.Network.createHTTPClient();
loadVideoXhr.onload = function() {
  var blipTVjson = eval(this.responseText);

  ...
    
  activityIndicator.show();
  videoPlayer.addEventListener('playing', function(e) {
    activityIndicator.hide();
  });
}

Next, we're going to add an event listener for when an orientationchange occurs on the iPad. In this anonymous function, we're going to resize the videoView's width and height, and reset the webView's top. This is so that we're always fitting the video width to the detailWindow's width, and then setting the other variables accordingly.

var loadVideoXhr = Ti.Network.createHTTPClient();
loadVideoXhr.onload = function() {
  var blipTVjson = eval(this.responseText);
  ...
  Titanium.Gesture.addEventListener('orientationchange', function(e){
    videoView.width = detailWindow.width;
    videoView.height = videoView.width / (16/9);
    webView.top = videoView.height;
  });
}  

Finally inside loadVideoXhr, we'll show the videoPlayer and play the video.

...
var loadVideoXhr = Ti.Network.createHTTPClient();
loadVideoXhr.onload = function() {
  var blipTVjson = eval(this.responseText);
  ...
  videoPlayer.show();
  videoPlayer.play();
}

Episode Detail View

In our iPhone app, we setup a click event listener on both the latest episodes table and the single topic table, which called the function loadRemoteMovie. In this iPad app, we're going to setup a click event listener on these tables with a named function called loadEpisodeDetailView.

...
latestTable.addEventListener("click", loadEpisodeDetailView);
singleTopicTable.addEventListener("click", loadEpisodeDetailView);

Before we create this function, we'll set a Boolean variable, firstVideo to true.

...
var firstVideo = true;

latestTable.addEventListener("click", loadEpisodeDetailView);
singleTopicTable.addEventListener("click", loadEpisodeDetailView);

Now let's make our loadEpisodeDetailView function. In it, we'll check if e.row.hasChild, just like in our iPhone app, which just checks to make sure we're clicking on an episode row, and not a blank row. Next we check if we're online with Ti.Network.online. If not, we'll call our notConnectedAlert() function, which presents an alert box to the user. We check the firstVideo boolean we created just a minute ago, which will be true the first time we hit this if-statement. If true, we'll add the videoView to the detailWindow, and set firstVideo to false. So the next time this function is called, we'll only stop the video player, instead of adding the videoView again, which we won't need to do. After this firstVideo check, we'll call loadRemoteMovie and loadWebView, passing through the episode object from the row passed into this function.

...
var firstVideo = true;

function loadEpisodeDetailView(e){
    if(e.row.hasChild){
        if (Ti.Network.online) {
            if(firstVideo) {
detailWindow.add(videoView);
                firstVideo = false;
            } else {
                videoPlayer.stop();
            }   
            loadRemoteMovie(e.row.episode);
            loadWebView(e.row.episode);
        } else {
            notConnectedAlert();
        }
    };

latestTable.addEventListener("click", loadEpisodeDetailView);
singleTopicTable.addEventListener("click", loadEpisodeDetailView);

loadRemoteMovie

Now let's create our loadRemoteMovie function above loadEpisodeDetailView.

Our loadRemoteMovie function will look similar again to what we had in our iPhone app. We'll open and send our loadVideoXhr HTTPClient. As we said before we need to make sure that we're on a Wi-Fi network since our video hosting service doesn't currently support HTTP Live streaming. So we have to make sure video playback only happens on a Wi-Fi network. We do this by checking if the networkType is of type Ti.Network.NETWORK_WIFI. If the user isn't on Wi-Fi, we will present an alert to notify them.

...
function loadRemoteMovie(episode){
  if(Ti.Network.networkType == Ti.Network.NETWORK_WIFI){
    loadVideoXhr.open('GET', 'http://blip.tv/players/episode/'+episode.video_blip_id+'?skin=json&callback=getVideoData&version=2');
    loadVideoXhr.send();    
  } else {
    var alertDialog = Ti.UI.createAlertDialog({
      title: 'Video requires wifi',
      message: 'Sorry, but video playback requires a wifi connection.',
      buttonNames: ['Continue']
    });
    alertDialog.show();
  }
};
...

loadWebView

In our loadWebView function, we'll set the url for our webView to a URL string that we'll build with episode.slug, plus a .app exenstion.

function loadWebView(episode) {  
    webView.url = 'http://screencasts.org/episodes/' + episode.slug + '.app';
}

episodesToData edit

To create this URL, we need to get the episode slug. To get it, we need to go back a few lines of code into our function episodesToData. In it, you'll see that we're creating row objects, setting several parameters. Our episode JSON that's coming into this function contains a bunch of information about each episode, including the slug we need. So let's add a new line inside createTableViewRow, setting episode to episode. So now this episode variable will be saved in each row.

...
function episodesToData(episodes) {
  var data = [];
  for (var i=0; i < episodes.length; i++) {
    var episode = episodes[i].episode;
    var row = Ti.UI.createTableViewRow({
      episode:episode,
      hasChild:true,
      height:80,
      backgroundColor:'#fff',
      video_blip_id:episode.video_blip_id
    });
    …
}

OK, great. So let's go back down to our loadWebView function.

Next, we have another boolean check to see if the welcome screen is showing with screencastsImageIsLoaded, and if true, we remove screencastsImage, and set the boolean to false. Then we set thetitleandbarColorfor ourdetailWindowto variables passed in from therowdata. Then wesetMasterPopupVisibletofalseon oursplitWindow, which removes the menu popover if the iPad is in portrait mode. Then we have to set it back totrue` again, so that the next time through this function, we're able to toggle it to false again. This may seem like odd behavior, but it's just how Titanium works apparently.

function loadWebView(episode) {
  webView.url = "http://screencasts.org/episodes/" + episode.slug + ".app";
  if(screencastsImageIsLoaded) {
    detailWindow.remove(screencastsImage);
    screencastsImageIsLoaded = false;
  }
  detailWindow.title = episode.title;
  detailWindow.barColor = episode.primary_topic.color;
  splitWindow.setMasterPopupVisible(false);
  splitWindow.setMasterPopupVisible(true);
}

If we launch the app again in Titanium, we'll now see that clicking on an episode row correctly opens the video and webView in our detailView. Great! We can see the show notes and full script for each episode, and watch the video full-screen if desired.

Conclusion

And that's it!

This app is available now in the App Store under the name 'Screencasts HD' (iTunes US | iTunes UK), so be sure to check it out there! If you're a fan of Screencasts.org, buying the iPhone (iTunes US | iTunes UK) and iPad (iTunes US | iTunes UK) apps are a great way to show your support.

Thanks for watching! Subscribe to our RSS feed, follow us on Twitter, and please leave any questions, comments or suggestions for new screencasts in the comments below. If you like our videos, and think your friends, followers or colleagues would benefit from seeing them, please feel free share via any of the links below the video. We really appreciate your support.

See you next time!

Update - 30 May 2011

Blip.tv have changed their API so we've pushed an update to the App Store.

To fix the code yourself, all you need to do is change:

...
function getVideoData(jsonData){
  return jsonData[0];
};
...

To:

...
function getVideoData(jsonData){
  return jsonData[0][0];
};
...

Update - 21 June 2011

Blip.tv have changed their API again. To fix use the following code:

...
function getVideoData(jsonData){
  return jsonData[0].Post;
};
...

← Latest Episodes

blog comments powered by Disqus