Beginning tvOS Development with TVML Tutorial
Learn how to create your first tvOS app for the Apple TV in this TVML tutorial for complete beginners! By Kelvin Lau.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Beginning tvOS Development with TVML Tutorial
30 mins
- Choose Your Adventure
- What is TVML?
- Getting Started
- Loading your TVML
- The JavaScript
- Client Code
- Setting up the Server
- Crafting the TVML
- Fleshing out the JavaScript Client
- Building the Catalog Template
- Compound and Simple Elements
- ResourceLoader
- Craft Some More TVML
- Completing the Template
- Playing Video
- Selection Events
- Where to Go From Here?
Last Wednesday Apple announced the new Apple-TV – along with what we’ve all been dreaming of, the ability to write our own apps for it!
I and the rest of the Tutorial Team have been digging into the tvOS SDK to prepare some great tvOS tutorials for you. To get you started, Chris Wagner wrote a post giving a broad overview of tvOS, and I’ve been working on this tutorial, designed to be your first steps in tvOS development.
In this tutorial, you’ll make your first tvOS app using TVML – Apple’s Television Markup Language. Believe it or not, you’ll use JavaScript to manage your app’s logic, and create TVML templates to present your UI.
By the end of the tutorial, you should have a basic grasp of managing tvOS apps using TVML and TVJS. Let’s get started!
Choose Your Adventure
Apple has provided developers two ways to develop tvOS apps:
- TVML Apps: The first uses an entirely new process utilizing TVML, TVJS and TVMLKit. I’ll explain what these abbreviations mean and how this works in a moment.
- Custom Apps: The second uses familiar iOS frameworks and concepts you know and love like Storyboards, UIKit, and Auto Layout.
Both ways are a completely valid way to make apps; it depends what you’re trying to do.
In this tutorial, your goal is to create this simple tvOS that streams RWDevCon conference videos:
Although you could create this app using either method, it is much easier to do so as a TVML app, so that is what you will be doing in this tutorial. To learn why, let me tell you a little bit more about how this works! :]
What is TVML?
As mentioned, the first method of making apps is via TVML, TVJS, and TVMLKit. If these abbreviations sound foreign to you, don’t fret because they should. Here’s what they are:
- TVML is a form of XML and stands for “Television Markup Language”.
- TVJS is set of JavaScript APIs which provide you with the means to display apps created with TVML.
- TVMLKit is the glue between TVML, JavaScript, and your native tvOS application.
If you’re a native iOS developer the thought of these might make you wince a little bit. But keep an open mind, there is some great power to all of this.
Here’s a very typical use-case for apps on Apple TV. Consider the following: you have content on a server and you want to display that content to users. Your content is organized in a predictable manner and navigating it should be intuitive and familiar. You want your tvOS app to feel at home with other apps. You’re not interested in pushing the envelope on cutting edge user experiences and designs.
This is exactly the situation we have in this tutorial. We already have a RWDevCon website that hosts the conference videos, so it would be quite easy to host some TVML templates there. We don’t have crazy requirements for the UI, so we can easily make use of some of Apple’s pre-made templates.
In short:
- Make a TVML App if you primarily provide menus of content, especially if you already have a server set up.
- Make a Custom App if you’re aiming to provide a fully immersive experience in your app, where your users will be spending more time interacting with your interface than passively watching or listening to content.
Now that you have a high-level understanding of how TVML works and why you’re using it in this tutorial, the best way to understand it further is to try it out yourself. Let’s start coding!
Getting Started
First make sure you have Xcode 7.1 or later installed and open on your machine.
Then go to File\New\Project and select the tvOS\Application\Single View Application template, and click Next:
For the Product Name enter RWDevCon, for the Language select Swift, make sure both checkboxes are unchecked, and click Next:
Choose a directory to save your project and click Save. Xcode will create a empty project for you with a Storyboard (which you would use if you were creating a tvOS Custom App).
However, you won’t need that because you are are making a TVMP app, which uses TVML files to display the UI rather than a Storyboard. So delete Main.storyboard and ViewController.swift from your project and select Move to Trash.
Next, head into the Info.plist and remove the Main storyboard file base name
key. Finally add a new value App Transport Security Settings
(case sensitive), and as its child, add Allow Arbitrary Loads
, and set that value to YES
.
Loading your TVML
The life cycle of the tvOS app starts with the app delegate. Here, you will set up the TVApplicationController
to pass control and the application context to the main JavaScript files.
Open AppDelegate.swift and do the following:
- Delete all the methods
- Import
TVMLKit
- Have your app delegate conform to
TVApplicationControllerDelegate
At this point your file should look like the following:
import UIKit
import TVMLKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, TVApplicationControllerDelegate {
var window: UIWindow?
}
Next, add the following variables to the class:
var appController: TVApplicationController?
static let TVBaseURL = "http://localhost:9001/"
static let TVBootURL = "\(AppDelegate.TVBaseURL)js/application.js"
TVApplicationController
is a class in TVMLKit
that handles communicating with your server. TVBaseURL
and TVBootURL
contains the paths for your server and JavaScript code, which you will be running on your localhost later.
Next add the following method to the class:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
// 1
let appControllerContext = TVApplicationControllerContext()
// 2
guard let javaScriptURL = URL(string: AppDelegate.TVBootURL) else { fatalError("unable to create NSURL") }
appControllerContext.javaScriptApplicationURL = javaScriptURL
appControllerContext.launchOptions["BASEURL"] = AppDelegate.TVBaseURL
// 3
appController = TVApplicationController(context: appControllerContext, window: window, delegate: self)
return true
}
This code is relatively straight forward:
- Here you create a
TVApplicationControllerContext
, which you will use to initialize yourTVApplicationController
. Think of this as a simple object you fill with information such as the URL of your server. - You fill the context with two bits of information: the path to your main Javascript file, and the root directory of your server.
- This starts up the
TVApplicationController
with the context you configured. At this point, Apple’s code takes over – it will pull down your root Javascript file and begin executing it.
And with that, it’s time to take a break from Xcode. Next, you’re going to write JavaScript!
The JavaScript
In the client-server tvOS application, your JavaScript code is typically contained in the server your app connects to. For the purposes of this tutorial, you’ll set up a simple server on your Mac.
Note: From now on, we’ll be working with JavaScript code. Personally, I stay away from using Xcode because of the indentation behaviours associated with working on blank Xcode files. Use an IDE of your choice. If you need a recommendation, I suggest Sublime Text 2 which you can download from here.
Client Code
For the sake of convenience, let’s put the JavaScript code on your Desktop. In your Desktop directory, create a new folder and name it client. Within the client directory, create a new folder and name it js. This folder will serve as the container for your JavaScript files.
With the IDE of your choice, create a new JavaScript file, name it application.js and save it to your js directory. Add the following to application.js
App.onLaunch = function(options) {
// 1
var alert = createAlert("Hello World", ""); //leaving 2nd parameter with an empty string
navigationDocument.presentModal(alert);
}
// 2
var createAlert = function(title, description) {
var alertString = `<?xml version="1.0" encoding="UTF-8" ?>
<document>
<alertTemplate>
<title>${title}</title>
<description>${description}</description>
</alertTemplate>
</document>`
var parser = new DOMParser();
var alertDoc = parser.parseFromString(alertString, "application/xml");
return alertDoc
}
App.onLaunch
is the method that handles the entry point of the JavaScript. The TVApplicationController
that was initialized in AppDelegate.swift will pass on its TVApplicationControllerContext
here. Later, you’ll make use of context’s contents, but for now, you’re just going to create a simple alert to display on screen.
- Using
createAlert
defined below, we get a TVML document for us to present. ThenavigationDocument
is analogous to aUINavigationController
in iOS; It serves as the stack that can push, pop, and present TVML documents. -
createAlert
is a function that returns a TVML document. You can consider it analogous to aUIAlertController
in iOS.
At the time of writing, Apple has 18 templates provided for us to use in our TVML apps – you can see the full list and specification in the Apple TV Markup Language Reference.
The alertTemplate used here is one of the 18, and its main purpose is to display important information, such as a message telling the user to perform an action before continuing. Finally, you’re almost ready for your first build and run!
Setting up the Server
Open the Terminal app and enter the following:
cd ~/Desktop/client
python -m SimpleHTTPServer 9001
This starts up a simple Python-based web server in your client directory. Now you’re cleared for takeoff!
Go back to your Xcode project and build and run. You should be greeted with your first tvOS TVML app!
I don’t know about you, but when I first got this working I felt like this guy:
Before moving forward, I’d like to spend time to appreciate the work you’ve done so far.
- You created a
TVApplicationController
. This manages the JavaScript code. - You created and attached a
TVApplicationControllerContext
to theTVApplicationController
. The context hadlaunchOptions
that was populated with ourBASEURL
which contained theURL
to the server. This context is also where the app finds out which server to connect to. - Control is passed to the JavaScript code.
App.onLaunch
kicks in and you returned a TVML alert template to present “Hello World” to the screen.
Note that even though you are running on your local web server, you could have put this on a live web server instead – perhaps hooked up to a database. Cool, eh?
Crafting the TVML
As I’ve pointed out before, createAlert
is a function that returns a TVML document. There are many more properties we can manipulate in a TVML document, and as an experiment, you’ll add a button to the current alertTemplate. Head back to your JavaScript code, and take a look at your current implementation of createAlert
. Add a button to the template:
var alertString = `<?xml version="1.0" encoding="UTF-8" ?>
<document>
<alertTemplate>
<title>${title}</title>
<description>${description}</description>
<button>
<text>OK</text>
</button>
</alertTemplate>
</document>`
Take a moment to appreciate the intricacies:
- A TVML document starts off by enclosing its contents with
document
/ - Next, you define the template. For the purposes of our
createAlert
function, we use the alertTemplate. - Within the template, you decorate it further with a button, a title, and a description, following the Apple TV Markup Language Reference.
Save your file, and build and run. You should see a button associated with your alert view. Voila, TVML made easy!
Note: The amount of elements you can put within a template vary depending on the specific template. For instance, a loadingTemplate does not allow any buttons. Furthermore, you can customize the font, color, and several other attributes of various items, but that is beyond the scope of this tutorial. A full list of each template’s capabilities can be found in the Apple TV Markup Language Reference.
Fleshing out the JavaScript Client
So far, you’ve got something going, and you’re well on our way to our goal. In this section, you’ll spend time abstracting the logic into different classes for better reusability.
Create a new JavaScript file in your client\js folder named Presenter.js. In this file, you’ll declare the class Presenter
that will handle the navigation stack. This class will be in charge of popping and pushing documents, and do event handling. Add the following to Presenter.js:
var Presenter = {
// 1
makeDocument: function(resource) {
if (!Presenter.parser) {
Presenter.parser = new DOMParser();
}
var doc = Presenter.parser.parseFromString(resource, "application/xml");
return doc;
},
// 2
modalDialogPresenter: function(xml) {
navigationDocument.presentModal(xml);
},
// 3
pushDocument: function(xml) {
navigationDocument.pushDocument(xml);
},
}
Let’s review this section by section:
- Remember that
DOMParser
is the class that can convert a TVML string into an object-oriented representation; you used this earlier increateAlert
. InDOMParser
you only want to create aDOMParser
once and reuse it multiple times, so you only create it if you haven’t already. You then add the same lines you added earlier to parse a TVML string and return the document. - The
modalDialogPresenter
method takes a TVML document and presents it modally on screen - The
pushDocument
method pushes a TVML document onto the navigation stack.
Later in the tutorial, you’ll have the Presenter
class manage cell selection as well. For now, let’s refactor the current JavaScript code to take Presenter
into account. Replace the current implementation of App.onLaunch
with the following:
App.onLaunch = function(options) {
// 1
var javascriptFiles = [
`${options.BASEURL}js/Presenter.js`
];
// 2
evaluateScripts(javascriptFiles, function(success) {
if(success) {
var alert = createAlert("Hello World!", "");
Presenter.modalDialogPresenter(alert);
} else {
// 3 Handle the error CHALLENGE!//inside else statement of evaluateScripts.
}
});
}
The code is relatively straightforward:
- Create a new array of JavaScript files. Recall earlier we passed in a
BASEURL
in thelaunchOptions
property of theTVApplicationControllerContext
. Now we will use it to create a path to the Presenter.js file. -
evaluateScripts
will load the JavaScript files - Here, you should handle the error. More on this in a second.
First, build and run to make sure that your code still works – now refactored to use your new Presenter
class:
Then see if you can solve the challenge indicated by the comment in section 3. If for some reason evaluateScripts
fails – perhaps because you mistyped the path to the JavaScript file(s) – you want to display an alert message. Hint: you cannot use the Presenter
class to do the presenting, since you’ve failed to load it.
You should be able to do this based on what you’ve learned so far. If you get stuck, check the solution below!
[spoiler title=”Solution to Challenge”]
//inside else statement of evaluateScripts.
var errorDoc = createAlert("Evaluate Scripts Error", "Error attempting to evaluate external JavaScript files.");
navigationDocument.presentModal(errorDoc);
To test your error message, change the value of the element inside javascriptFiles
. For example, delete a letter: `${options.BASEURL}js/Presentr.js`
. (deleted the last e)
[/spoiler]
Building the Catalog Template
The catalogTemplate is another one of the 18 templates that are available for developers to use. The purpose of the template is to display information about groups of like products, which is perfect for showcasing your favorite RWDevCon videos! The catalogTemplate has many elements of interest:
Compound and Simple Elements
The banner
element is used to display information along the top of the template app page. It itself is a Compound Element, meaning it is composed of several Simple Elements.
For instance, the obvious use case for the banner is to add a title
element, but it can also have a background
element. For the purposes of our tutorial, we’ll keep the customizations as little as possible. At the end of the tutorial, there will be a link for further reading regarding other elements.
Let’s try this out. Navigate to your client directory, and create 2 new folders as siblings to the js folder, and name them images and templates respectively. Your client folder should now look like this:
You’ll need images to populate the cells in our template. I’ve prepared the images for you: download them, unzip the file, and move the images to the images folder you’ve just created.
Now, you’re going to display the images on screen! Create a new JavaScript file, name it RWDevConTemplate.xml.js, and save it in the templates folder.
Add the following to RWDevConTemplate.xml.js:
var Template = function() { return `<?xml version="1.0" encoding="UTF-8" ?>
<document>
<catalogTemplate>
<banner>
<title>RWDevConHighlights</title>
</banner>
</catalogTemplate>
</document>`
}
For now, we’ll attempt to display the banner of the template. Before we can use this code, since this isn’t currently exposed to the other JavaScript files, we need a way to let the other files know of its existence. A great time to create our last JavaScript file: ResourceLoader.js!
ResourceLoader
Create a new JavaScript file, name it ResourceLoader.js, and save it in the js folder, along with your application.js and Presenter.js files. Add the following to the file:
function ResourceLoader(baseurl) {
this.BASEURL = baseurl;
}
ResourceLoader.prototype.loadResource = function(resource, callback) {
var self = this;
evaluateScripts([resource], function(success) {
if(success) {
var resource = Template.call(self);
callback.call(self, resource);
} else {
var title = "Resource Loader Error",
description = `Error loading resource '${resource}'. \n\n Try again later.`,
alert = createAlert(title, description);
navigationDocument.presentModal(alert);
}
});
}
Don’t worry too much about how this works; just know you can use this to load other template files.
Try it out by replacing your “Hello World” alert with our newly created RWDevConTemplate
as the main screen. Open application.js and make the following changes to the file:
// 1
var resourceLoader;
App.onLaunch = function(options) {
// 2
var javascriptFiles = [
`${options.BASEURL}js/ResourceLoader.js`,
`${options.BASEURL}js/Presenter.js`
];
evaluateScripts(javascriptFiles, function(success) {
if(success) {
// 3
resourceLoader = new ResourceLoader(options.BASEURL);
resourceLoader.loadResource(`${options.BASEURL}templates/RWDevConTemplate.xml.js`, function(resource) {
var doc = Presenter.makeDocument(resource);
Presenter.pushDocument(doc);
});
} else {
var errorDoc = createAlert("Evaluate Scripts Error", "Error attempting to evaluate external JavaScript files.");
navigationDocument.presentModal(errorDoc);
}
});
}
// Leave createAlert alone
You’ve made 3 changes here:
- Declared a
resourceLoader
variable. - Added ResourceLoader.js to the list of files we want to expose.
- Used the
resourceLoader
to load the TVML template, and used thePresenter
to present it on screen.
Build and run. You should be greeted with the following screen:
Congratulations, you are now able to load TVML from a file, rather than hard-coding it into your Javascript! Cue the return of my friend:
Craft Some More TVML
Believe it or not, but you’re almost done. One of the most beautiful things about TVML tvOS apps is that it’s very easy to add UI elements. What you’re about to add to your RWDevConTemplate may seem a lot, but it’s really a fraction of what you would have to do using UIKit
frameworks.
Modify the RWDevConTemplate.xml.js file with the following:
var Template = function() { return `<?xml version="1.0" encoding="UTF-8" ?>
<document>
<catalogTemplate>
<banner>
<title>RWDevConHighlights</title>
</banner>
//add stuff here
//1.
<list>
<section>
//2.
<listItemLockup>
<title>Inspiration Videos</title>
<decorationLabel>13</decorationLabel>
</listItemLockup>
</section>
</list>
</catalogTemplate>
</document>`
}
- You’ve defined the list area, which encompasses the rest of the screen’s contents
- The
listItemLockup
represents a section cell. Each cell is defined by alistItemLockup
tag. You’ve declared the title to be “Inspiration Videos”, and added a number next to it, to indicate the number of items you’re going to display for this section.
Build and run. You should see the following screen on the simulator:
Not bad for just a little markup!
Completing the Template
Finally, we’re ready to create our cells that will represent each video. Add the following to RWDevConTemplate.xml.js:
//This file outlines the catalogTemplate.
var Template = function() { return `<?xml version="1.0" encoding="UTF-8" ?>
<document>
<catalogTemplate>
<banner>
<title>RWDevConHighlights</title>
</banner>
<list>
<section>
<listItemLockup>
<title>Inspiration Videos</title>
<decorationLabel>13</decorationLabel>
//1. add from here
<relatedContent>
<grid>
<section>
//2
<lockup videoURL="http://www.rwdevcon.com/videos/Ray-Wenderlich-Teamwork.mp4">
<img src="${this.BASEURL}images/ray.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Ryan-Nystrom-Contributing.mp4">
<img src="${this.BASEURL}images/ryan.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Matthijs-Hollemans-Math-Isnt-Scary.mp4">
<img src="${this.BASEURL}images/matthijs.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Vicki-Wenderlich-Identity.mp4">
<img src="${this.BASEURL}images/vicki.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Alexis-Gallagher-Identity.mp4">
<img src="${this.BASEURL}images/alexis.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Marin-Todorov-RW-Folklore.mp4">
<img src="${this.BASEURL}images/marin.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Chris-Wagner-Craftsmanship.mp4">
<img src="${this.BASEURL}images/chris.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Cesare-Rocchi-Cognition.mp4">
<img src="${this.BASEURL}images/cesare.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Ellen-Shapiro-Starting-Over.mp4">
<img src="${this.BASEURL}images/ellen.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Jake-Gundersen-Opportunity.mp4">
<img src="${this.BASEURL}images/jake.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Kim-Pedersen-Finishing.mp4">
<img src="${this.BASEURL}images/kim.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Tammy-Coron-Possible.mp4">
<img src="${this.BASEURL}images/tammy.png" width="500" height="308" />
</lockup>
<lockup videoURL="http://www.rwdevcon.com/videos/Saul-Mora-NSBrief.mp4">
<img src="${this.BASEURL}images/saul.png" width="500" height="308" />
</lockup>
</section>
</grid>
</relatedContent>
</listItemLockup>
</section>
</list>
</catalogTemplate>
</document>`
}
- You’ve added the
relatedContent
tag. This refers to the following area:
- Each
lockup
tag represents a cell in thegrid
We’ve included avideoURL
property for eachlockup
. This will be necessary to stream the videos from the RWDevCon website.
Build and run. You’ve brought your app to life!
Now that we’ve got many different cells to play around with, let’s bring out the remote controller in the simulator (if you haven’t already). With the Simulator window active, click Hardware\Show Apple TV Remote. You can move around the cells by simply holding the option key and moving your cursor on the remote window.
Playing Video
So far, we’ve got the page populated, and it looks great. Once again, think about the many things you would’ve done to get this layout to work with iOS frameworks. Apple really did a good job abstracting all the details by providing us with these fantastic templates to work with.
Let’s move on to implement the remaining two features for this app: cell selection, and media playback.
Selection Events
You may have noticed already, but pressing the enter key or clicking the Apple TV Remote gives the pressed down animation, but nothing else happens. Now you’re going to implement the necessary code to implement cell selection.
You’re going to have Presenter
handle this. Add the following method to the Presenter
class:
load: function(event) {
//1
var self = this,
ele = event.target,
videoURL = ele.getAttribute("videoURL")
if(videoURL) {
//2
var player = new Player();
var playlist = new Playlist();
var mediaItem = new MediaItem("video", videoURL);
player.playlist = playlist;
player.playlist.push(mediaItem);
player.present();
}
},
-
The
load
method is responsible for cell selection. It is analogous to an@IBAction
, where theevent
argument is similar to thesender
argument. Eachevent
has atarget
. For our purposes, thetarget
refers to eachlockup
element. Remember, eachlockup
element represents our cells that display the video thumbnail, and they all have avideoURL
property. - Displaying a media player is simple. The class
Player
of the TVJS framework provides all the media playback functionality. All you need is to add aplaylist
, and amediaItem
into the playlist. Finally, theplayer.present()
will put the video on screen
Now that you’ve got the implemented the logic to respond to selection events, it’s time to actually hook it up to each cell! For the last time, head back to the application.js file, and add the following line in the App.onLaunch
method:
App.onLaunch = function(options) {
//...
//inside resourceLoader.loadResource...
var doc = Presenter.makeDocument(resource);
doc.addEventListener("select", Presenter.load.bind(Presenter)); //add this line
Presenter.pushDocument(doc);
//...
}
The addEventListener
method is analogous to hooking a button to an @IBAction
. Build and run. Choose a video to play. You should be greeted by the media player:
You can download the completed tutorial project here: client and RWDevCon
Where to Go From Here?
You’ve covered a lot of ground today. You’ve learned the basic architecture of a tvOS client-server app. You’ve learned how to manage TVML, use TVJS, and use TVMLKit to connect to a server. For some of you, this is the first time you’ve handled XML and JavaScript files. You have a lot to be proud of!
If you enjoyed this tutorial, you should check out our book the tvOS Apprentice. With 28 chapters and 538 pages, the book teaches you everything you need to know to develop great apps for the Apple TV – whether you’re a seasoned iOS pro, or a web developer looking to leverage your skills to a new platform.
Are you excited about the future of tvOS? Please join us in the forum discussion below!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more