In our beginning tvOS development tutorial, you created your first-ever TVML app. You saw how simple it can be to start developing for the Apple TV, and how easy it is to play video in a fully-featured player. However, the UI design of your app left a bit to be desired.
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.
In the process, you’ll learn how to use TVML templates to make great looking user interfaces, and you’ll learn how to use a templating engine to populate pages (rather than hard-coded data). Let’s get started!
Getting Started
The app you’ll build in this tutorial is called 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.
I’ve prepared a starter project for you that includes the resources you’ll need for this tutorial. Download the starter project, unzip, and build and run 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
In the previous tutorial, you learned how to split your JavaScript code across several files. The project for wenderTV already has a starter file to help you load app resources (ResourceLoader.js), so you need to import this into the Javascript file you load in application(_:didFinishLaunchingWithOptions:)
(main.js).
Remember that 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 (from the helper file provived), 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 helper 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:
If you get a prompt that says video.tvml already exists, choose Replace.
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:
Hm. 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 we should review a bit of background on on TVML templates.
TVML Templates
Remember that 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 <document>
element, which is a single element at the top level of the document and the parent (or ancestor) of all other elements. The <document>
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 on-screen, 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.
This tutorial will cover a few of the templates, and then you should have enough of an understanding to use any templates you’d like in your apps.
The Product Template
The first document you’ll create uses <productTemplate>
, 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 <productTemplate>
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<info>
elements, arranged in a vertical list. -
<info>
: A container for the content to appear as an item in an<infoList>
or an<infoTable>
. -
<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:
You can almost make out the new entries down the left-hand side, but the current background color makes it difficult to see the white text.
Update the opening tag of <productTemplate>
to match the following:
<productTemplate theme="light"> |
The theme
attribute can either be dark
or light
and can be used on many TVML templates. It controls the visual effect applied to the background and the colors used in the foreground fonts.
Build and run again; you’ll see the content clearly:
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 </infoList>
closing tag:
<stack> <title>Teamwork</title> <row> <text>17m 54s</text> <text>Inspiration</text> <text>2015</text> <badge src="resource://nr" /> <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<infoList>
. 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>
: Displays 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 data and 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.
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 the render
method 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 <info>
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 <text>
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!
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! Now that you’ve provided a hero image for tvOS to use when it generates the background, you can investigate the effects of the theme
attribute.
Find the opening <productTemplate>
tag and change the theme
attribute from light
to dark
. Build and run again to see the difference:
The visual effect on the background has changed along with the foreground font colors.
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 <monogramLockup>
and <monogram>
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 (use the Apple TV remote to scroll):
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. Choose Replace if prompted. 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 straighforward to understand. There’s the usual XML prologue, the <descriptiveAlertTemplate>
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 <description>
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 the event argument represents the DOM object that fired the event. ThegetAttribute()
method 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 the
presentModal()
method onNavigationDocument
to display the new document on top of the current document. -
If the action is set to
dismiss
then use thedismissModal()
method 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); |
As you saw in the previous tutorial, this registers the _handleEvent
function 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.
Now that is one swell-looking – and extensible – interface.
Where To Go From Here?
Here is the example code from this TVML tutorial.
In this tutorial, you accomplished a lot – you created a great-looking TVML app using TVML templates, and integrated a Javascript templating engine to separate the UI from the data.
If you’d like to learn more, you should check out our book the tvOS Apprentice. The chapter in the book goes into further detail and show you how to make the TVML to allow users to rate videos from 1-5 stars. You also might enjoy the other 27 chapters and 500+ pages in the book! :]
In the meantime, if you have any questions or comments about using TVML templates, please join the forum discussion below!
The post tvOS Tutorial: Using TVML Templates appeared first on Ray Wenderlich.