David Cordero

iOS and tvOS developer at @Zattoo. Amateur Triathlete. Passionate about coding and lifelong learning.

TVML and UIKit as happy roommates

06 Jul 2017 » swift, tvos

There is a classical beginning for every tvOS tutorial and it is a question: Are you creating a TVKit application or a native UIKit application?

Depending on the answer to that question, you will end up with one of two completely different ways of working to create your application.

The point is that the answer to that question is usually not so easy, because in fact, both alternatives, TVKit or UIKit, have their advantages and disadvantages.

📺 TVKit: Is a great solution for fast prototyping. Apple provides a super beautiful set of TVML templates, so that with just a few lines of code you can get a visually attractive application, completely integrated with the system. On the other hand, TVML templates are basically like they are, and they do provide a quite low level of customization.

💄 UIKit: On the other hand, offers a wider level of customisation. But it lacks of those fancy templates, so everything that you need will need to be created manually.

Would it not be great if we could just mix them up? So we could have, at the same time, a very customisable App with the option of fast prototyping specific sections making use of TVML?

image

Detached TVML:

One of the required steps to start working with TVML is to setup an instance of TVApplicationController. This instance of TVApplicationController is the one that will allow us to send the control of the app to JS.

In general, TVML Apps create their instance of TVApplicationController directly in their AppDelegate, and that is basically all the swift code that they have:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)

    let appControllerContext = TVApplicationControllerContext()
    let appJS = Bundle.main.url(forResource: "application", withExtension: "js")

    if let javaScriptURL = appJS {
        appControllerContext.javaScriptApplicationURL = javaScriptURL
    }

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

As you can see, one of the parameters received by the constructor of TVApplicationController is the application window.

The point is that when setting that window parameter to our TVApplicationController, it will actually set the rootViewController of the application to the one defined by our TVKit code.

But, what if we do not bind the instance of TVApplicationController to the window at that point, using nil instead for the window parameter?

Then we will get a detached instance of TVApplicationController that is not bound to the window. And thanks to the property navigationController exposed by TVApplicationController, which is just a standard UINavigationController, we can manually host it as we want.

This is a very simple example of a UIKit ViewController presenting a TVKit screen when the IBAction sendMeToTVMLButtonWasPressed is triggered:

import UIKit
import TVMLKit

class MyUIKitViewController: UIViewController {

    lazy var appController: TVApplicationController = {
        let appControllerContext = TVApplicationControllerContext()
        let appJS = Bundle.main.url(forResource: "application", withExtension: "js")

        if let javaScriptURL = appJS {
            appControllerContext.javaScriptApplicationURL = javaScriptURL
        }

        // Here is the thing. Setting nil to the window creates a deatached the application controller
        return TVApplicationController(context: appControllerContext,
                                                window: nil,
                                                delegate: nil)
    }()

    @IBAction func sendMeToTVMLButtonWasPressed(_ sender: Any) {

        // Manually presenting the TVML screen making use of its navigationController
        present(appController.navigationController, animated: true, completion: nil)
    }
}

And that is all, with this simple method we can use UIKit and TVKit in the same App, making use of one or the other whenever they fit better.

TVML as UIViewController:

Attending to the fact that having our views implemented with UIKit or TVKit is just an implementation detail, would it not be great if we could get rid of those particularities, having for them a similar interface?

This is actually what we can get embedding our TVKit code in a UIKit container view, so it is exposed with the interface of a UIViewController.

import UIKit
import TVMLKit

class TVMLViewController: UIViewController, TVApplicationControllerDelegate {

    private var containerView: UIView!

    var appController: TVApplicationController?

    override func viewDidLoad() {
        setUpTVMLAppController()
        setUpContainerView()
    }

    // MARK: - Private

    private func setUpContainerView() {
        guard let tvmlViewController = appController?.navigationController else { return }
        containerView = UIView()
        containerView.frame = view.bounds
        addChildViewController(tvmlViewController)
        view.addSubview(tvmlViewController.view)
    }

    // Create an instance of TVApplicationController, loading content
    // from a local file application.js included in the Application Bundle
    private func setUpTVMLAppController() {
        let appControllerContext = TVApplicationControllerContext()
        let appJS = Bundle.main.url(forResource: "application", withExtension: "js")

        if let javaScriptURL = appJS {
            appControllerContext.javaScriptApplicationURL = javaScriptURL
        }

        // Here is the magic. Setting nil to the window creates a deatached the application controller
        appController = TVApplicationController(context: appControllerContext,
                                                window: nil,
                                                delegate: self)
    }
}

Show me the code:

If you are interested in more details about this, I have prepared this simple project combining UIKit and TVKit, embedding the TVML screen in a UIKit ViewController so it can be presented like any other ViewController.

let tvmlViewController = TVMLViewController()
present(tvmlViewController, animated: true, completion: nil)

And just for fun… I have also added some JS to the TVKit screen, binding it with some asynchronous code in Swift to emulate some network communication.

Conclusion:

UIKit is great, it has been there since forever giving us a lot of powerful and flexibility, but whether you like it or not Apple is investing a lot in TVKit and this new way of working.

The best part is that TVKit and UIKit are definitely not exclusive, and we can mix them up taking advantage of the benefits of each whenever we need them.

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