David Cordero

Extending TVML with custom templates

Published on 7 Jun 2018

TVML templates are the base of every App based on TVMLKit. They ease the creation of Apps following the UI patterns of tvOS.

Apple provides a set of TVML templates, exactly 18 of them: alertTemplate, catalogTemplate, compilationTemplate, descritiveAlertTemplate, divTemplate, formTemplate, listTemplate, loadingTemplate, mainTemplate, menuBarTemplate, oneupTemplate, paradeTemplate, productBundleTemplate, productTemplate, ratingTemplate, searchTemplate, showcaseTemplate and stackTemplate

The divTemplate is by far the most flexible one. It basically allows throwing into it most of the TVML components and to align them with a high level of flexibility. But it also has some limitations, for example not being scrollable.

So, what to do if none of the templates provided by Apple fits our requirements?

For example, let’s imagine that we would like to create the following screen in our App.

scrollable data picker

This is a good example of a very simple screen, that could not be easily represented with any of the 18 templates provided by Apple.

So, at this point we would have two options:

• To create a hybrid App, mixing up TVML and UIKit, as we saw in TVML and UIKit as happy roommates

• To create our custom TVML Template, as we will see right now.

(This second method has not been officially documented by Apple. This post is the result of some investigation with the public API of TVMLKit. On the other hand, this has been validated and approved as a legitimate method by Apple Engineers in WWDC Labs 2018 and it was described as something that they would like to document officially in the near future.)

Creation of our screen as ViewController

The first step would be to create the desired screen using UIKit, which is a very easy task using a UITableView or even UITableViewController if you wish.

You can find below a possible implementation based on UITableView:

import UIKit
import TvOSCustomizableTableViewCell

class GreetingSelectorViewController: UIViewController, UITableViewDataSource {
    
    var items: [String] = []
    
    private var titleLabel: UILabel!
    private var tableView: UITableView!
    
    // MARK: UIViewController
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpTitleLabel()
        setUpTableView()
    }
    
    // MARK: UITableViewDataSource
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TvOSCustomizableTableViewCell
        
        cell.textLabel?.text = items[indexPath.row]
        
        cell.focusedBackgroundColor = .orange
        cell.normalBackgroundColor = .darkGray
        cell.normalTitleColor = .white
        cell.focusedScaleFactor = 1.02
        cell.textLabel?.textAlignment = .center
        
        return cell
    }
    
    // MARK: Private
    
    private func setUpTitleLabel() {
        titleLabel = UILabel()
        titleLabel.font = UIFont.systemFont(ofSize: 50)
        titleLabel.text = title
        view.addSubview(titleLabel)
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 130).isActive = true
        titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    }
    
    private func setUpTableView() {
        tableView = UITableView()
        tableView.dataSource = self
        tableView.rowHeight = 100
        tableView.register(TvOSCustomizableTableViewCell.self, forCellReuseIdentifier: "Cell")
        view.addSubview(tableView)
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 300).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -150).isActive = true
        tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 400).isActive = true
        tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -400).isActive = true
    }
}

Notice that due to the fact that UITableViewCells are barely customizable in tvOS, I made use of TvOSCustomizableTableViewCell to get the desired layout.

TVInterfaceFactory and TVInterfaceCreating

Once that we created a ViewController to represent the desired screen, the next step would be to expose it as a TVML template.

Whenever we need to extend TVML, TVInterfaceFactory (implementing TVInterfaceCreating) is the way to go.

So let’s start creating our custom subclass of TVInferfaceFactory from which we will return an instance of our viewController, whenever TVMLKit asks for it with via its template name (greetingSelectorTemplate).

import TVMLKit

class CustomInterfaceFactory: TVInterfaceFactory {
    
    static let templateName = "greetingSelectorTemplate"
    
    override func makeViewController(element: TVViewElement, existingViewController: UIViewController?) -> UIViewController? {
        guard element.name == CustomInterfaceFactory.templateName else { return nil }
        let greetingsViewController = existingViewController as? GreetingSelectorViewController ?? GreetingSelectorViewController()
        greetingsViewController.title = "Select your favorites greeting:"
        greetingsViewController.items = ["Hallo", "Hola", "Hello", "Acho", "Gruetzi", "🙋‍♂️", "Hej", "Sveiks"]

        return greetingsViewController
    }
}

(Notice that a previously created instance might be received in the parameter existingViewController for its reusability.)

We also need to inform the system about the existence of our custom TvInterfaceFactory. To do so, we need to define extendedIntefaceCreator in the shared instance of TvInterfaceFactory.

This should be done only once, so it might be added to AppDelegate, for example.

TVInterfaceFactory.shared().extendedInterfaceCreator = CustomInterfaceFactory()
TVElementFactory.registerViewElementClass(TVViewElement.self, elementName: CustomInterfaceFactory.templateName)

And that is all that we need to do. From now on we can create a TVML document using our custom template id (greetingSelectorTempale) getting as a result our brand new custom ViewController.

<?xml version="1.0" encoding="UTF-8" ?>
<document>
    <greetingSelectorTemplate>
    </greetingSelectorTemplate>
    </document>

Custom TVML attributes and styles

So far, we are able to present a ViewController implemented in UIKit as a TVML template, but it will be always presented with exactly the same appearance, without any option to customize its layout and with the same listed elements on it.

Let’s iterate our example to support the customization of the title with a value provided from TVML:

override func makeViewController(element: TVViewElement, existingViewController: UIViewController?) -> UIViewController? {

    guard element.name == CustomInterfaceFactory.templateName else { return nil }
    let greetingsViewController = GreetingSelectorViewController()

    if let title = element.attributes?["title"] {
        greetingsViewController.title = title
    }

return greetingsViewController
}

And that is all that we need to do, from now on we can customize the title of our template from our TVML document, by just adding a title property to its definition.

<?xml version="1.0" encoding="UTF-8" ?>
<document>
    <greetingSelectorTemplate title="My custom Title">
    </greetingSelectorTemplate>
    </document>

Further customization

Our custom template is now like any other native template. That means that we could customize it in the same way as the templates provided by Apple.

So, apart from customizing the title, we might add internal elements or styling attributes. It is out of the scope of the current post, nevertheless if you are interested about this, I strongly recommend this great post by Alex Guretzki.

Show me the code

In the following URL, you can find an example of a custom TVML Template (confettiTemplate), using the method previously described:

https://github.com/dcordero/CustomTVMLTemplate

Conclusion

TVML is a quite limited framework, and it is easy to face its limitations on the first steps of your project, especially when trying to represent some information in a different way than its set of predefined templates.

Nevertheless, as we have seen, this should not be a limitation, and we could create our custom suite of templates matching our desired layouts.

If we do it properly, thinking of reusability, we might end up with a very useful set of custom templates that could save a lot of time in future developments.

Feel free to follow me on github, twitter or dcordero.me if you have any further question.