race-conditiontvostvmltvjs

TVOS: Race condition at startup


Using the templates and TVML, I start my app with my own loading page, and then call a service to create the main page for the user.

If I initiate the call to the server inside didFinishLaunchingWithOptions, I get the error ITML <Error>: undefined is not an object - undefined - line:undefined:undefined.

From this I assume my asynchronous call to the server is finishing before the javascript App.onLaunch function has completed, and I can only get it to work if I force a wait time before the server is called.

Here is the AppDelegate method:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        window = UIWindow(frame: UIScreen.mainScreen().bounds)
        let appControllerContext = TVApplicationControllerContext()

        // our "base" is local
        if let jsBootUrl = NSBundle.mainBundle().URLForResource("application", withExtension: "js") {
            appControllerContext.javaScriptApplicationURL = jsBootUrl
        }
        let jsBasePathURL = appControllerContext.javaScriptApplicationURL.URLByDeletingLastPathComponent
        baseUrl = jsBasePathURL?.absoluteString
        appControllerContext.launchOptions["BASEURL"] = jsBasePathURL?.absoluteString

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

        // initiate conversation with the server
        myPageCreator = PageCreator()
        myPageCreator?.delegate = self
        myPageCreator?.startDataCall(baseUrl!)

        return true
    }

Here is the (somewhat boilerplate) javascript function:

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

    evaluateScripts(javascriptFiles, function(success) {
        if (success) {

            resourceLoader = new ResourceLoader(options.BASEURL);
            var index = resourceLoader.loadResource(`${options.BASEURL}myLoadingPage.xml.js`,
                function(resource) {
                    var doc = Presenter.makeDocument(resource);
                    doc.addEventListener("select", Presenter.load.bind(Presenter));
                    navigationDocument.pushDocument(doc);
                });
        } else {
            /* handle error case here */
        }
    });
}

Now, if I change the call to the server in the didFinishLaunchingWithOptions, and force it to wait, like this:

        ...
        // race condition hack:
        _ = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "testing", userInfo: nil, repeats: false)

        return true
    }

    // initiate conversation with the server
    func testing() {
        myPageCreator = PageCreator()
        myPageCreator?.delegate = self
        myPageCreator?.startDataCall(baseUrl!)
    }

.. it will work. But I don't like that solution! What can I do to stop this race condition from happening?


Solution

  • You need a way for Javascript to communicate with Swift so that you know when App.onLaunch has finished running it's scripts.

    Run this code in your didFinishLaunchingWithOptions method. It will allow you to call onLaunchDidFinishLoading() in Javascript and handle the callback in Swift.

    appController.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in
       let onLaunchDidFinishLoading : @convention(block) () -> Void = {
          () -> Void in
             //when onLaunchDidFinishLoading() is called in Javascript, the code written here will run.
             self.testing()
       }
       evaluation.setObject(unsafeBitCast(onLaunchDidFinishLoading, AnyObject.self), forKeyedSubscript: "onLaunchDidFinishLoading")
    
       }, completion: {(Bool) -> Void in
    })
    
    
    func testing() {
        myPageCreator = PageCreator()
        myPageCreator?.delegate = self
        myPageCreator?.startDataCall(baseUrl!)
    }
    

    Inside App.onLaunch just add onLaunchDidFinishLoading() when the template is done being loaded.

    App.onLaunch = function(options) {
    var javascriptFiles = [
        `${options.BASEURL}ResourceLoader.js`,
        `${options.BASEURL}Presenter.js`
    ];
    
    evaluateScripts(javascriptFiles, function(success) {
        if (success) {
    
            resourceLoader = new ResourceLoader(options.BASEURL);
            var index = resourceLoader.loadResource(`${options.BASEURL}myLoadingPage.xml.js`,
                function(resource) {
                    var doc = Presenter.makeDocument(resource);
                    doc.addEventListener("select", Presenter.load.bind(Presenter));
                    navigationDocument.pushDocument(doc);
                    //ADD THE FOLLOWING LINE
                    onLaunchDidFinishLoading();
                });
        } else {
            /* handle error case here */
        }
    });
    }