Introduction to TVOS

Login to Access Code

Setup Xcode

Download/open Xcode 7.1 (or later)

Create a new project in Xcode: File > New > Project and select tvos > Application > Single View Application.

New TVOS Project in Xcode

You can then delete your ViewController.swift and Main.Storyboard from your project, as we won’t be using them.

Delete (move to trash) Main.Storyboard and ViewController.swift

Next you’ll want to update Info.plist to reflect the deletion of the storyboard as well as allow insecure (non-https) requests.

Update Info.plist

Delete the Main storyboard file base name entry from Info.plist

Add the App Transport Security Settings entry from Info.plist

Add Allow Arbitrary Loads entry under Transport Security Settings and set value to YES

Finally, you’ll need to modify AppDelegate to support a TVOS Application. Replace your contents with the following:

import UIKit
import TVMLKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, TVApplicationControllerDelegate {

  var window: UIWindow?

  var appController: TVApplicationController?


  func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    window = UIWindow(frame: UIScreen.mainScreen().bounds)

    // Create the TVApplicationControllerContext for this application
    let appControllerContext = TVApplicationControllerContext()

    // The JavaScript URL is used to create the JavaScript context for your TVMLKit application
    if let url = NSURL(string: "http://localhost:4000/client.js") {
      appControllerContext.javaScriptApplicationURL = url
    }

    // Set the properties that will be passed to the `App.onLaunch` function in JavaScript
    if let launchOptions = launchOptions as? [String: AnyObject] {
      for (kind, value) in launchOptions {
        appControllerContext.launchOptions[kind] = value
      }
    }

    appController = TVApplicationController(context: appControllerContext, window: window, delegate: self)


    return true
  }

}

Building the TVJS Application

This project leverages the rails backend from a previous screencast: Building a Rails CMS with API. You can go see the raw response and demo application to make your application more dynamic. Since this is up for others to play with, here is a snapshot of the response:

{
  "videos": [
    {
      "_id": {
        "$oid": "562177faad19dc000c000000"
      },
      "category": "ruby",
      "cover": "https://s3-us-west-1.amazonaws.com/unicorn.tv/tvos+images/Unconventional-Rails.lcr",
      "description": "Learn how to build a Content Management System and Cache it's API",
      "mp4": "https://player.vimeo.com/external/139777685.hd.mp4?s=825fedfe776c5999fd272a030b3d8c9d",
      "name": "Build a Rails CMS and API"
    }
  ]
}

You could digest any API however, as you will see in the client code below. AJAX is very easy to perform with TVJS and rendering with TVML is incredibly easy. The client code below is the real backbone of this application, and acts as the controller to the view (the Xcode app) and the model (the API).

App.onLaunch = function(options) {
  // Create an object to store our catalog entries in
  var data = {
    "swift"      : null,
    "ruby"       : null,
    "go"         : null,
    "javascript" : null
  };

  // Take an XML string and turn it into a parse into a proper DOM
  function parse(xml) {
    var parser = new DOMParser();

    return parser.parseFromString(xml, "application/xml");
  }

  // A quick utility for displaying a loading page
  function displayLoading() {
    var loading = `<?xml version="1.0" encoding="UTF-8" ?>
    <document>
     <loadingTemplate>
        <activityIndicator>
           <title>Loading...</title>
        </activityIndicator>
     </loadingTemplate>
    </document>`;

    navigationDocument.pushDocument(parse(loading));
  }

  // Display video media in full-screen
  function displayVideo(event) {
    var url = event.target.getAttribute("video", url);

    if (url) {
      var player = new Player();
      var playlist = new Playlist();
      var mediaItem = new MediaItem("video", url);

      player.playlist = playlist;
      player.playlist.push(mediaItem);
      player.present();
    }
  }

  // Refresh the catalog and render the navigation and video list
  function refreshCatalog() {
    if (data["swift"] && data["ruby"] && data["go"] && data["javascript"]) {
      var catalog = '<?xml version="1.0" encoding="UTF-8" ?><document><catalogTemplate><banner><title>Manticore TV</title></banner><list>';

            for (topic in data)
            {
        console.log('looping through topic ' + topic);

                catalog += `<section>
                    <listItemLockup>
                        <title>${topic}</title>
                        <decorationLabel>${data[topic]["videos"].length}</decorationLabel>
                        <relatedContent>
                            <grid>
                                <section>`;

        for (i = 0; i < data[topic]["videos"].length; i++)
        {
          console.log('looping through videos');
          console.log(data[topic]["videos"][i]);

          if (data[topic]["videos"][i].mp4) {
            console.log('has video, create lockup');
            catalog += `<lockup video="${data[topic]["videos"][i].mp4}">
              <img src="${data[topic]["videos"][i].cover}" width="550" height="275" />
              <title>${data[topic]["videos"][i].name}</title>
            </lockup>`;
          }
        }

        catalog += `</section>
                            </grid>
                        </relatedContent>
                    </listItemLockup>
                </section>`;
      }

      catalog += `</list></catalogTemplate></document>`;
      var catalogDoc = parse(catalog);
      catalogDoc.addEventListener("select", displayVideo);
      navigationDocument.pushDocument(catalogDoc);
    }
  }

  // Fetch data from an API / server
  function fetchData(topic) {
    var httpRequest = new XMLHttpRequest();
    httpRequest.onreadystatechange = function()
    {
      if (httpRequest.readyState === 4) {
        if (httpRequest.status === 200) {
          data[topic] = JSON.parse(httpRequest.responseText);

          console.log(data[topic]["videos"][0].mp4);
          refreshCatalog();
        }
      }
    }

    httpRequest.open('GET', 'https://rails-api-cache.herokuapp.com/api/videos.json');
    httpRequest.send();
  }

  // Commented out for demo, but useful to use for error messages and loading statuses
  // displayLoading();

  // Loop through the topics and fetch the results from the API
  for (topic in data) {
    fetchData(topic);
  }
}

To run this code, simply save as client.js and serve it up with a basic server. Below is a basic python server that will do the trick:

python -m SimpleHTTPServer 4000

Then you can build and run the Xcode application, it will load your client.js file locally which will fetch videos from the Rails API example on heroku.