tvOS Tutorial: Using TVML Templates
In this tvOS tutorial, you’ll learn how to use TVML templates and templating engines to make great-looking user interfaces. By Chris Belanger.
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
tvOS Tutorial: Using TVML Templates
35 mins
- Getting Started
- Loading Scripts
- TVML Templates
- The Product Template
- Data Injection
- Reading JSON From the App Bundle
- Injecting Data Into the Document String
- Resolving Image URLs
- Using the Mustache.js Templates
- Template Sections
- Filling out the TVML Template
- Adding Shelves
- Handling Text Overflow
- Event Handling
- Viewer Ratings
- Collecting Ratings
- Where to Go From Here?
In this tutorial, you’ll learn how to use the plethora of TVML templates that Apple has provided to make some stunning interfaces. You’ll use these templates to build a comprehensive screen for RWDevCon 2015 videos, which will include a wide range of information about the video and display it in an appealing and recognizable manner.
Getting Started
You can download the starter project for this tutorial here.
The sample app for this tutorial is wenderTV; it lets you browse and watch raywenderlich.com video content on your Apple TV, so you can enjoy a vast quantity of knowledge – and bad jokes – from the comfort of your sofa. Right now, wenderTV is quite empty.
wenderTV includes a lot of resources you’ll use during this tutorial; we’ve also reorganized some of the code to make it easier to understand.
Build and run the wenderTV starter project from Xcode; you’ll notice the screen is completely blank. This is because there aren’t any TVML documents in wenderTV yet. Before you can turn your attention to creating some TVML documents, you have a little housekeeping to do.
Loading Scripts
The project for wenderTV already has ResourceLoaderJS
split out into its own file, so you need to discover how to import it into main.js.
TVJS provides the evaluateScripts()
function which takes an array of URLs of JavaScript files and a callback. The function reads each URL in turn, attempts to parse it and adds any contained functions and object definitions to the context’s global object. Finally, it invokes the supplied callback with a Boolean denoting success or failure.
The AppDelegate
contains code that provides the JavaScript application with a list of URLs to the required JavaScript files as part of the launch options object. Open main.js and add the following code to the App.onLaunch()
function:
// 1:
evaluateScripts(options.initialJSDependencies,
function(success){
if (success) {
// 2:
resourceLoader =
new ResourceLoaderJS(NativeResourceLoader.create());
var initialDoc = loadInitialDocument(resourceLoader);
navigationDocument.pushDocument(initialDoc);
} else {
// 3:
var alert = _createAlert("Evaluate Scripts Error",
"Error attempting to evaluate the external JS files.");
navigationDocument.presentModal(alert);
throw ("Playback Example: unable to evaluate scripts.");
}
});
Taking this new function body step-by-step:
-
The
options
object is prepared in Swift and then passed to the JavaScript app in the app delegate, while theinitialJSDependencies
property is an array of URLs for the different JavaScript files that need to be loaded as the app starts.evaluateScript()
performs this action and then invokes the callback indicating whether it was successful. -
If the JavaScript sources evaluated successfully, create a
ResourceLoaderJS
object before using it to load the initial document and then pushing this TVML document onto the navigation stack.loadInitialDocument()
is currently a stub method you’ll populate in a bit. -
If the JavaScript files failed to load, there is nothing more that the app can do. Therefore it uses
_createAlert()
, defined at the bottom of main.js to build and present an alert document and then throw an error.
Add the following to loadInitialDocument()
:
return resourceLoader.getDocument("video.tvml");
This uses the resource loader to get the TVML file from the app’s resource bundle and return it as a DOM object.
Next up, you’ll need to create the file your app is trying to load: video.tvml.
Head into Xcode, and right-click on the layouts group in the project navigator. Select New File…:
Navigate to tvOS\Other\Empty and hit Next. Name the file video.tvml and ensure that the wenderTV target is checked:
Open the new file in either your favorite XML editor (or Xcode, if you insist) and add the following lines:
<?xml version="1.0" encoding="UTF-8" ?>
<document>
<productTemplate>
</productTemplate>
</document>
This defines a simple TVML document using a new type of template: productTemplate
.
Build and run the app to see how things look so far:
Hmm. The screen is still blank. Although you’ve created the file and provided the template, you haven’t provided any content. You’ll fix that shortly, but first you need to cover a bit of background on TVML templates.
TVML Templates
A TVML document is just an XML (eXtentible Markup Language) document with a specific schema defined by Apple. If you’ve used HTML in the past, XML will look familiar. HTML isn’t actually XML (despite some failed efforts with XHTML), but the syntax and structure is fairly similar.
Since TVML documents are XML they should start with the following line:
<?xml version="1.0" encoding="UTF-8" ?>
This is known as the XML prologue; it notes this file is an XML document and which character encoding it uses.
The TVML document has a root
element, which is a single element at the top level of the document and the parent (or ancestor) of all other elements. The
element has a single direct descendant, which can be one of 18 possible template tags.
A template tag specifies the top-level layout tvOS should use to render the document on screen. In addition to specifying the appearance onscreen, template tags also convey some semantic information about the content they contain. Templates might look similar, but have entirely different purposes.
TVML templates can be divided into the following categories:
- Informational: Shows a small amount of information to the user, and optionally requests input from the user. It’s not designed for browsable content. Includes alertTemplate and loadingTemplate.
- Data Entry: The user experience of entering data on TVs is pretty horrendous, and Apple TV is no different. However, there are a few templates for requesting data from the user, including searchTemplate and ratingTemplate.
- Single Item: Displays information or content about a single product or item, such as a film or episode in a TV series. Includes productTemplate, oneupTemplate, compilationTemplate and showcaseTemplate.
- Collections: Displays a collection of products, such as a TV series, a genre of films or tracks on an album. Includes stackTemplate, listTemplate and productBundle.
- Other: Includes menuBarTemplate, which hosts a set of other templates, and divTemplate, which is a completely clean slate upon which you draw.
The Product Template
The first document you’ll create uses
, which is designed to display all information relating to a specific product — in this case, a video.
Open video.tvml and add the following code between the
tags:
<banner>
<infoList>
<info>
<header>
<title>Presenter</title>
</header>
<text>Ray Wenderlich</text>
</info>
<info>
<header>
<title>Tags</title>
</header>
<text>development</text>
<text>teams</text>
<text>business</text>
</info>
</infoList>
</banner>
This code snippet introduces a lot of new element types. Taking them one-by-one:
-
<banner>
: Displays content across the top of a page. -
<infoList>
: Displays a list of
elements, arranged in a vertical list. -
<info>
: Acts as a container forthis s the content to appear as an item in an
or an
. -
<header>
: Serves as a description of the content of the section in which it resides. -
<title>
: Contains the text of a short title. -
<text>
: Displays text.
Build and run the app to see what your new TVML looks like:
This page represents a video, but it currently lacks a title. Time to change that.
Add the following inside the <banner>
tags, just after the closing tag:
<stack>
<title>Teamwork</title>
<row>
<text>17m 54s</text>
<text>Inspiration</text>
<text>2015</text>
<badge src="resource://nr" />is
<badge src="resource://cc" />
<badge src="resource://hd" />
</row>
</stack>
This section introduces more TVML elements:
-
<stack>
: Stacks lay out their content vertically down the screen in a manner similar to
. There’s a wider range of tags that can be in a Stack. -
<row>
: A row is like a stack, but with a horizontal orientation instead of vertical. -
<badge>
: Badges display a small inline image. The URL is provided by thesrc
attribute.
Notice that the URL of the two badge images begin with resource://
. This is a special URL scheme that points to images that exist within tvOS itself. These images include common action icons, such as “play”, rating images for different countries and video information such as HD.
Build and run again to see how the page is shaping up:
It’s starting to look good, but there’s still a long way to go. Before continuing with the template coding, you first need to consider the separation of the data and the view.
Data Injection
As your video document currently stands, all the data is hard-coded. To show information about a different video, you’d have to create a whole new page. If you wanted to reformat the video page once you’ve created all the pages, you’d have to go back through and edit every single one of them.
A much better approach is to use a templating engine, where you build the video page as a template and specify where the data should be injected. At runtime, the template engine takes the page template along with the data and generates the TVML page for tvOS to display.
Mustache.js is a popular simple templating engine for JavaScript. You might recognize the templating syntax which is based around curly-braces:
{{property-name}}
The Mustache.js library is already part of wenderTV, but you need to build the mechanisms to use it. This presents you with several tasks to accomplish:
- The data is stored as JSON files in the app bundle. The JavaScript app needs the ability to request them.
- When a document is loaded, it now requires data, and this should be combined with the document string using Mustache.js.
- Images that are present in the app bundle need their complete URL substituted.
- The video document should be updated to turn it into a templated document.
You’ll address each of these in order.
However, prototyping is only available at the <section>
level, while you’re using it in this tutorial for much more, such as in the <header>
section of your TVML. You’ll continue to use Mustache.js as a templating engine for this section of the book, but if you end up having to handle large content sets in your tvOS apps, read up on prototyping and data binding at apple.co/2utDXXm.
Reading JSON From the App Bundle
Open ResourceLoader.js and add the following method to ResourceLoaderJS
:
getJSON(name) {
var jsonString = this.nativeResourceLoader
.loadBundleResource(name);
var json = JSON.parse(jsonString);
return json;
}
This function uses the native resource loader to pull the JSON file from the app bundle before parsing it into a JavaScript object.
Injecting Data Into the Document String
Now that you can obtain the data for a given page, you need to combine it with the document template itself. Update getDocument()
in ResourceLoaderJS
to match the following:
getDocument(name, data) {
data = data || {};
var docString = this.nativeResourceLoader
.loadBundleResource(name);
var rendered = Mustache.render(docString, data);
return this.domParser.parseFromString(rendered,
"application/xml");
}
Here you’ve added an additional data
argument to the method and used render
on Mustache
to convert the template and data to a completed document. As before, you use a DOMParser
to convert the document string to a DOM object.
Resolving Image URLs
For simplicity’s sake, your sample data stores images as the names of files in the app bundle, which need to be converted to URLs before you can display them. The utility functions to do this are already in the resource loader, so you just need to call them. You’ll do this at the same time as you update the initial document loading to use the templating engine.
Open main.js and update loadInitialDocument()
to match the following:
function loadInitialDocument(resourceLoader) {
var data = resourceLoader.getJSON("teamwork.json");
data["images"] = resourceLoader
.convertNamesToURLs(data["images"]);
data = resourceLoader
.recursivelyConvertFieldsToURLs(data, "image");
data["sharedImages"] = _sharedImageResources(resourceLoader);
return resourceLoader.getDocument("video.tvml", data);
}
First, you load the data using the new getJSON()
method. Then you use the utility functions to perform the image name-to-URL conversion. These convert three different image name sources:
-
Each value in the
images
object on the data array. -
Every value associated with a key of
image
anywhere within the JSON data structure. - A set of shared images that are useful for all documents in wenderTV.
That takes care of the plumbing underneath; you’re ready to use this powerful new functionality.
Using the Mustache.js Templates
Open teamwork.json and take a look at the data you’ll use to populate the video page. There’s quite a lot of data, but it’s a standard JSON object and fairly easy to understand. You should spot some fields such as title
, presenter
and duration
that you’ve already hard-coded into video.tvml. You’re now going to swap these out.
Open video.tvml and find the title tag that contains Ray Wenderlich. Replace the name with {{presenter}}
, so that the first
section now looks like this:
<info>
<header>
<title>Presenter</title>
</header>
<text>{{presenter}}</text>
</info>
The syntax for Mustache.js is really simple; it will replace {{presenter}}
with the value of presenter
in the data object supplied to it.
Now that you’ve got the hang of that, you can replace the following content with the respective template tags:
-
Teamwork
→{{title}}
-
17m 54s
→{{duration}}
-
Inspiration
→{{category}}
-
2015
→{{year}}
-
resource://nr
→resource://{{rating}}
Build and run; you shouldn’t see any difference, which is exactly what you want. The page is now data-driven, and even better, you didn’t break anything. Bonus! :]
There are still some parts of the template you haven’t touched: closed-captions, HD and tags. These use some slightly more advanced parts of the Mustache.js templating engine.
Template Sections
Remove the three
tags in the Tags section and add the following in their place:
{{#tags}}
<text>{{.}}</text>
{{/tags}}
This new syntax defines a template section. Look at teamwork.json and you’ll see that tags
is an array of strings. The Mustache.js syntax here loops through the array, with {{.}}
rendering the content of the current index.
Finally, you need to handle the two Boolean badges. Replace the cc and hd badges with the following:
{{#closed-captions}}
<badge src="resource://cc" />
{{/closed-captions}}
{{#hd}}
<badge src="resource://hd" />
{{/hd}}
Once again you’re using sections, but this time they’re structured like an if
statement. If the specified property exists and has a true
value, then render this section; otherwise, ignore it.
Build and run again; check out your newly templated video page.
To confirm that the data injection is actually working, open main.js and change the data file loaded in loadInitialDocument()
from teamwork.json to identity.json. Build and run again to see the data change.
You can now see details of Vicki’s talk on identity — sweet!
on
Filling out the TVML Template
The video page is still looking a little barren. It’s time to double-down on adding some content.
Open video.tvml and add the following inside the <stack>
, just below the existing <row>
:
<description allowsZooming="true"
moreLabel="more">{{description}}</description>
<text>{{language}}</text>
<row>
<buttonLockup type="play">
<badge src="resource://button-play" />
<title>Play</title>
</buttonLockup>
<buttonLockup type="buy">
<text>$9.99</text>
<title>Buy</title>
</buttonLockup>
</row>
Once again, this introduces some new TVML elements:
-
<description>
: Displays a larger amount of text that’s used to describe content. If the text is too long for the display area then a label will be displayed with a title defined by themoreLabel
attribute. -
<buttonLockup>
: A lockup is a class of element that joins its children together as a single element. A button lockup can contain text and a badge and will appear as a button.
Remember that these elements are all contained within a <stack>
, so they’ll appear on top of each other.
Before checking your work, you need to add one more element to the top banner. Add the following line immediately after the </stack>
closing tag:
<heroImg src="{{images.hero}}" />
A heroImg
element is a large image that defines the content of this document. It appears inside the <banner>
and tvOS uses it to define the blurred page background.
Build and run to see the completed top banner:
It’s starting to look really cool – But wait! You may now have noticed that the text color has changed from black to white. How did that happen?
tvOS has decided that because the background image you specified is darker than a certain threshold, it needed to increase the contrast of your text, so it changed the default text color.
Update the <productTemplate>
tag to match the following:
<productTemplate theme="light">
Build and run to see the difference:
The visual effect on the background has changed along with the foreground font colors. You can change the theme
attribute to dark
if you wish to force the previous look if you prefer it.
theme
to get the effect you’re used to. Also, please don’t get this behavior confused with the new dark mode introduced by Apple in tvOS 10. That will be covered in more detail later in the book.
Adding Shelves
The remainder of the productTemplate
is made up of “shelves”. A shelf is a horizontal section of the page with content elements scrolling on and off the screen.
Add the following below the closing </banner>
tag, towards the bottom of video.tvml:
<shelf>
<header>
<title>You might also like</title>
</header>
<section>
{{#recommendations}}
<lockup>
<img src="{{image}}" width="402" height="226" />
<title>{{title}}</title>
</lockup>
{{/recommendations}}
</section>
</shelf>
This shelf displays a set of other videos that the user might enjoy as defined in the recommendations
property of the data model. Each recommendation has an image
and a title
, each of which you use in the code above.
There are two other elements introduced in this code segment:
-
<section>
: Defines a set of related content that should all be laid out together. A section can contain a title and multiple lockup elements. -
<lockup>
: You saw<buttonLockup>
before;lockup
is a more general type of lockup. It provides layout for an image, a title, a badge and a description.
Now add another shelf below the one you just created:
<shelf>
<header>
<title>Production</title>
</header>
<section>
{{#people}}
<monogramLockup>
<monogram firstName="{{firstname}}"
lastName="{{lastname}}"/>
<title>{{firstname}} {{lastname}}</title>
<subtitle>{{role}}</subtitle>
</monogramLockup>
{{/people}}
</section>
</shelf>
This shelf displays a list of people associated with the production; it’s stored in the people
property in the data model.
This introduces the
and
elements, which let you represent a person when an avatar isn’t available. Like the other lockup elements, a monogram lockup simply locks its content together.
A monogram has firstName
and lastName
attributes, from which it generates a monogram (initials) and places it in a large circle.
Build and run to see how your shelves are taking shape. You will have to scroll down to reveal the lower shelf:
Take a look at the description for the “Identity” talk. Notice that the more label has appeared, because there is too much text to display in the available space. Navigate to the label and you’ll see you can focus on the description and press select to trigger an action. This action doesn’t currently do anything, but wouldn’t it be nice if it would display the full text?
Time for another TVML template.
Handling Text Overflow
The descriptive alert template provides space for an extended area of text and buttons. It sounds ideal for this purpose. You’ll use this template and a spot of JavaScript to wire it up.
In the Xcode project, right-click on the layouts group and select New File…. Choose tvOS\Other\Empty and name the file expandedDetailText.tvml.
Open the new file and add the following:
<?xml version="1.0" encoding="UTF-8" ?>
<document>
<descriptiveAlertTemplate>
<title>{{title}}</title>
<description>{{text}}</description>
<button action="dismiss">
<text>Dismiss</text>
</button>
</descriptiveAlertTemplate>
</document>
This should be quite straightforward to understand. There’s the usual XML prologue, the
tag and some elements you’ve used before. Notice that the button tag has an action
attribute; this is a user-defined attribute that’s not part of the TVML specification.
You can define any attributes that you want (provided they don’t clash with existing attributes) and then read them from your JavaScript app. You’ll write some code to handle this dismiss
action in just a bit.
Open video.tvml and find the
tag. Update the element to match the following:
<description allowsZooming="true"
moreLabel="more"
action="showOverflow"
title="{{title}}">{{description}}</description>
You’ve added two new attributes: action
and title
. You’ll use both of these in the event handler to create the expanded detail text document.
Event Handling
Now that the document templates are ready to go you can turn your attention to the JavaScript that wires everything up.
Open main.js and add the following function:
function _handleEvent(event) {
// 1:
var sender = event.target;
var action = sender.getAttribute("action");
// 2:
switch(action) {
case "showOverflow":
// 3:
var data = {
text: sender.textContent,
title: sender.getAttribute("title")
};
// 4:
var expandedText = resourceLoader
.getDocument("expandedDetailText.tvml", data);
expandedText.addEventListener("select", _handleEvent);
navigationDocument.presentModal(expandedText);
break;
case "dismiss":
// 5:
navigationDocument.dismissModal();
break;
}
}
Taking this piece-by-piece:
-
The
target
property of theevent
argument represents the DOM object that fired the event.getAttribute()
of a DOM object will return the value for the specified attribute. Here you’re using it to find the value of theaction
attribute you added above. -
Switch on the
action
attribute to invoke the appropriate code. -
If the action is
showOverflow
, then you have a description field with too much content. Construct an object with the data required by the expanded detail text document. Once again you’re usinggetAttribute()
along withtextContent
, which returns the content of the tag itself. -
Load the expandedDetailText.tvml document in the usual way, add an event listener and use
presentModal()
onNavigationDocument
to display the new document on top of the current document.
-
If the action is set to
dismiss
, usedismissModal()
onNavigationDocument
to perform the dismissal.
Now that you’ve created this event handler, you need to wire it up to the initial document. Add the following line to App.onLaunch
, just after you call loadInitialDocument()
:
initialDoc.addEventListener("select", _handleEvent);
This registers _handleEvent
as a listener for the select event, and uses event bubbling to handle all events triggered within the document.
Build and run the app, navigate down to the over-full description and hit the select button. You’ll see your new expanded detail text page:
You can use the dismiss button to return to the video screen.
Viewer Ratings
In the final part of this tutorial, you’ll add a new ratings section to the video page and let the user select a rating using a new template.
Open video.tvml and add the following new shelf underneath the Production shelf:
<shelf>
<header>
<title>What other people thought</title>
</header>
<section>
{{#ratings-reviews}}
<ratingCard action="addRating">
{{#rw-ratings}}
<title>{{out-of-five}} / 5</title>
<ratingBadge value="{{badge-value}}"></ratingBadge>
<description>Mean of {{count}} ratings.</description>
{{/rw-ratings}}
</ratingCard>
{{#reviews}}
<reviewCard>
<title>{{title}}</title>
<description>{{description}}</description>
<text>{{name}} {{date}}</text>
</reviewCard>
{{/reviews}}
{{/ratings-reviews}}
</section>
</shelf>
By now, you’ll recognize most of the TVML elements, but there are still a few new ones:
-
<ratingCard>
: Displays a small card suitable for showing the ratings of a product. -
<ratingBadge>
: A specialized badge for showing a star-rating. Thevalue
attribute should be a value between 0 and 1, which will be converted to a proportion of five stars. -
<reviewCard>
: A card for displaying user or critic reviews.
Notice that the
element has the custom action
attribute again. You’ll use this later to display the rating page.
Build and run to see what the new shelf looks like:
When the user selects the rating card, you want to let them choose a rating for the current video. This is exactly what the ratings TVML template is for.
Collecting Ratings
In Xcode, right-click on the layouts group and select New File…. Choose tvOS\Other\Empty and name the file videoRating.tvml.
Open the new file and add the following:
<?xml version="1.0" encoding="UTF-8" ?>
<document>
<ratingTemplate>
<title>{{title}}</title>
<ratingBadge />
</ratingTemplate>
</document>
This new file uses the
which simply displays and collects ratings. It contains a <title>
and a <ratingBadge>
, both of which you’ve already seen.
The template is ready; you just need to display it. Open main.js and add the following case
to the switch
statement in _handleEvent()
:
case "addRating":
var ratingDoc = resourceLoader.getDocument("videoRating.tvml",
{title: "Rate Video"});
navigationDocument.presentModal(ratingDoc);
break;
These few lines load the new document you created and provide the title to the templating engine. It then displays the document modally.
Build and run, navigate to the rating card and hit select to see the new ratings page:
Now that is one swell-looking – and extensible – interface.
Where to Go From Here?
You can download the final project from this tutorial here.
In this tutorial you’ve created a great-looking TVML app and used three of the built-in templates along with a vast array of TVML-specific elements. You also integrated a JavaScript templating engine to separate the UI from the data. Adopting great development techniques right from the start is a win in anyone’s book.
You can check out Apple’s documentation (apple.co/1PJuOAV) for specifics about the templates and elements covered in this tutorial.
If you enjoyed what you learned in this tutorial, why not check out the complete tvOS Apprentice book, available in our store?
Here’s a taste of what’s in the book:
Section I: Architecture
This section is designed to give you a birds-eye view of how tvOS works and help you decide what to read next.
Section II: TVML Apps
This section covers the basics for creating an app via the TVML approach. From the basics of Hello World through a real world example, by the end of this section you’ll know everything you need to create client / server apps for Apple TV.
Section III: Traditional Apps
This section covers the basics for creating apps via the traditional approach. You’ll learn the new libraries created for Apple TV, and how the ported libraries from iOS can be used.
Section IV: Advanced Frameworks
This section covers some of the more advanced frameworks you’ll need for many TV app use cases. Whether you took the TVML approach or the Traditional approach, these frameworks will be important to understand to make your app stand out.
Section V: Design
This section covers design concepts important for tvOS. For your app to stand apart from the rest, you’ll need to understand these design concepts well.
Bonus Chapter
And that’s not all — on top of the above, we have a bonus chapter for you that gives you a crash course in JavaScript!
By the end of this book, you’ll have some great hands-on experience with building exciting, good-looking apps for the Apple TV!
And to help sweeten the deal, the digital edition of the book is on sale for $49.99! But don’t wait — this sale price is only available for a limited time.
Speaking of sweet deals, be sure to check out the great prizes we’re giving away this year with the iOS 11 Launch Party, including over $9,000 in giveaways!
To enter, simply retweet this post using the #ios11launchparty hashtag by using the button below:
We hope you enjoy this update, and stay tuned for more book releases and updates!