Search code examples
xcodetvostvmltvjs

Can you host TVJS files on the Apple TV instead of an external server?


I've downloaded the TVMLCatalog application from Apple. The code is split up into 2 parts.

  1. client - this holds the TVML and TVJS files
  2. TVMLCatalog Project - this is the basic Xcode project that sets up the TVML/TVJS

I'm attempting to host the client TVJS files in the same bundle as the TVMLCatalog Project.

I've changed the AppDelegate didFinishLaunching as follows:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
    window = UIWindow(frame: UIScreen.mainScreen().bounds)

    /*
    Create the TVApplicationControllerContext for this application
    and set the properties that will be passed to the `App.onLaunch` function
    in JavaScript.
    */
    let appControllerContext = TVApplicationControllerContext()

    /*
    The JavaScript URL is used to create the JavaScript context for your
    TVMLKit application. Although it is possible to separate your JavaScript
    into separate files, to help reduce the launch time of your application
    we recommend creating minified and compressed version of this resource.
    This will allow for the resource to be retrieved and UI presented to
    the user quickly.
    */

    TVBootURL = NSBundle.mainBundle().pathForResource("application", ofType: "js")!
    TVBaseURL = TVBootURL.stringByReplacingOccurrencesOfString("application.js", withString: "")
    if let javaScriptURL = NSURL(string: TVBootURL) {
        appControllerContext.javaScriptApplicationURL = javaScriptURL
    }

    appControllerContext.launchOptions["BASEURL"] = TVBaseURL

    if let launchOptions = launchOptions as? [String: AnyObject] {
        for (kind, value) in launchOptions {
            appControllerContext.launchOptions[kind] = value
        }
    }

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

    return true
}

Here is a screenshot that demonstrates how I've imported the client: Xcode Screenshot

When I run the project (only tested on simulator) I get the following message displayed on the AppleTV simulator screen:

Error launching Application - The operation couldn't be completed. (TVMLKitErrorDomain error 3.)

Can I load from TVJS files locally like this?


Solution

  • I was able to find the answer after some deep googling. This person's post really helped me:

    http://thejustinwalsh.com/objective-c/tvml/2015/09/20/tvml-without-the-webserver.html

    The example is in objective-c but I've implemented a Swift solution.

    Here is how I changed my code from the original post:

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        window = UIWindow(frame: UIScreen.mainScreen().bounds)
    
        /*
        Create the TVApplicationControllerContext for this application
        and set the properties that will be passed to the `App.onLaunch` function
        in JavaScript.
        */
        let appControllerContext = TVApplicationControllerContext()
    
        /*
        The JavaScript URL is used to create the JavaScript context for your
        TVMLKit application. Although it is possible to separate your JavaScript
        into separate files, to help reduce the launch time of your application
        we recommend creating minified and compressed version of this resource.
        This will allow for the resource to be retrieved and UI presented to
        the user quickly.
        */
    
        if let javaScriptURL = NSBundle.mainBundle().URLForResource("application", withExtension: "js"){
            appControllerContext.javaScriptApplicationURL = javaScriptURL
        }
    
        let TVBaseURL = appControllerContext.javaScriptApplicationURL.URLByDeletingLastPathComponent
    
        appControllerContext.launchOptions["BASEURL"] = TVBaseURL?.absoluteString
    
        if let launchOptions = launchOptions as? [String: AnyObject] {
            for (kind, value) in launchOptions {
                appControllerContext.launchOptions[kind] = value
            }
        }
    
        appController = TVApplicationController(context: appControllerContext, window: window, delegate: self)
    
        return true
    }
    

    One important note: You'll need to change the filepath references in your TVJS files to reflect the new bundle path structure.

    example in Application.js:

    App.onLaunch = function(options) {
    var javascriptFiles = [
        `${options.BASEURL}js/ResourceLoader.js`,
        `${options.BASEURL}js/Presenter.js`
    ];
    ...
    

    becomes:

    App.onLaunch = function(options) {
    var javascriptFiles = [
        `${options.BASEURL}ResourceLoader.js`,
        `${options.BASEURL}Presenter.js`
    ];
    ...
    

    and this path:

    ${options.BASEURL}templates/Index.xml.js

    becomes:

    ${options.BASEURL}Index.xml.js

    [UPDATE]

    Swift 3

    Important: Add your application.js file to the project's target; this is not added by default when starting a new project.

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        window = UIWindow(frame: UIScreen.main.bounds)
    
        // Create the TVApplicationControllerContext for this application and set the properties that will be passed to the `App.onLaunch` function in JavaScript.
        let appControllerContext = TVApplicationControllerContext()
    
        // The JavaScript URL is used to create the JavaScript context for your TVMLKit application. Although it is possible to separate your JavaScript into separate files, to help reduce the launch time of your application we recommend creating minified and compressed version of this resource. This will allow for the resource to be retrieved and UI presented to the user quickly.
        if let javaScriptURL = Bundle.main.url(forResource: "application", withExtension: "js"){
            appControllerContext.javaScriptApplicationURL = javaScriptURL
        }
    
        let TVBaseURL = appControllerContext.javaScriptApplicationURL.deletingLastPathComponent()
    
        appControllerContext.launchOptions["BASEURL"] = TVBaseURL.absoluteString
    
        if let launchOptions = launchOptions {
            for (kind, value) in launchOptions {
                appControllerContext.launchOptions[kind.rawValue] = value
            }
        }
    
        appController = TVApplicationController(context: appControllerContext, window: window, delegate: self)
    
        return true
    }