How to bridge TVML/JavaScriptCore to UIKit/Objective-C (Swift)?
Asked Answered
M

2

21

So far tvOS supports two ways to make tv apps, TVML and UIKit, and there is no official mentions about how to mix up things to make a TVML (that is basically XML) User Interface with the native counter part for the app logic and I/O (like playback, streaming, iCloud persistence, etc).

So, which is the best solution to mix TVML and UIKit in a new tvOS app?

In the following I have tried a solution following code snippets adapted from Apple Forums and related questions about JavaScriptCore to ObjC/Swift binding. This is a simple wrapper class in your Swift project.

import UIKit
import TVMLKit
@objc protocol MyJSClass : JSExport {
    func getItem(key:String) -> String?
    func setItem(key:String, data:String)
}
class MyClass: NSObject, MyJSClass {
    func getItem(key: String) -> String? {
        return "String value"
    }

    func setItem(key: String, data: String) {
        print("Set key:\(key) value:\(data)")
    }
}

where the delegate must conform a TVApplicationControllerDelegate:

typealias TVApplicationDelegate = AppDelegate
extension TVApplicationDelegate : TVApplicationControllerDelegate {

    func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) {
        let myClass: MyClass = MyClass();
        jsContext.setObject(myClass, forKeyedSubscript: "objectwrapper");
    }

    func appController(appController: TVApplicationController, didFailWithError error: NSError) {
        let title = "Error Launching Application"
        let message = error.localizedDescription
        let alertController = UIAlertController(title: title, message: message, preferredStyle:.Alert ) self.appController?.navigationController.presentViewController(alertController, animated: true, completion: { () -> Void in
            })
        }

    func appController(appController: TVApplicationController, didStopWithOptions options: [String : AnyObject]?) {
    }

    func appController(appController: TVApplicationController, didFinishLaunchingWithOptions options: [String : AnyObject]?) {
    }
}

At this point the javascript is very simple like. Take a look at the methods with named parameters, you will need to change the javascript counter part method name:

   App.onLaunch = function(options) {
       var text = objectwrapper.getItem()
        // keep an eye here, the method name it changes when you have named parameters, you need camel case for parameters:      
       objectwrapper.setItemData("test", "value")
 }

App. onExit = function() {
        console.log('App finished');
    }

Now, supposed that you have a very complex js interface to export like

@protocol MXMJSProtocol<JSExport>
- (void)boot:(JSValue *)status network:(JSValue*)network user:(JSValue*)c3;
- (NSString*)getVersion;
@end
@interface MXMJSObject : NSObject<MXMJSProtocol>
@end
@implementation MXMJSObject
- (NSString*)getVersion {
  return @"0.0.1";
}

you can do like

JSExportAs(boot, 
      - (void)boot:(JSValue *)status network:(JSValue*)network user:(JSValue*)c3 );

At this point in the JS Counter part you will not do the camel case:

objectwrapper.bootNetworkUser(statusChanged,networkChanged,userChanged)

but you are going to do:

objectwrapper.boot(statusChanged,networkChanged,userChanged)

Finally, look at this interface again:

- (void)boot:(JSValue *)status network:(JSValue*)network user:(JSValue*)c3;

The value JSValue* passed in. is a way to pass completion handlers between ObjC/Swift and JavaScriptCore. At this point in the native code you do all call with arguments:

dispatch_async(dispatch_get_main_queue(), ^{
                                           NSNumber *state  = [NSNumber numberWithInteger:status];
                                           [networkChanged.context[@"setTimeout"]
                                            callWithArguments:@[networkChanged, @0, state]];
                                       });

In my findings, I have seen that the MainThread will hang if you do not dispatch on the main thread and async. So I will call the javascript "setTimeout" call that calls the completion handler callback.

So the approach I have used here is:

  • Use JSExportAs to take car of methods with named parameters and avoid to camel case javascript counterparts like callMyParam1Param2Param3
  • Use JSValue as parameter to get rid of completion handlers. Use callWithArguments on the native side. Use javascript functions on the JS side;
  • dispatch_async for completion handlers, possibly calling a setTimeout 0-delayed in the JavaScript side, to avoid the UI to freeze.

[UPDATE] I have updated this question in order to be more clear. I'm finding a technical solution for bridging TVML and UIKit in order to

  • Understand the best programming model with JavaScriptCode
  • Have the right bridge from JavaScriptCore to ObjectiveC and viceversa
  • Have the best performances when calling JavaScriptCode from Objective-C
Maffa answered 12/10, 2015 at 12:47 Comment(7)
This is not a question, as far as I can tell. If you've found some useful information you want to share, ask and answer your own question. Also, I think this topic has ben brought up somewhere in tvos or apple-tvos already, so you probably don't need to ask a new question, just answer an existing one.Cece
@ricksterIf I would have found an answer to this question, I would have answered it, but so far not. There is no specific question about tvOS and TVML + UIKIT so I do not get your point. Yes, maybe the question is not clear, and I could specify better. Your answer is "not constructive" since tvOS is a brand new technology with few knowledge on Stackoverflow. Of course this is my point of view, I'm pretty sure that the question is "constructive" anyways.Maffa
If this posting isn't an attempt to provide information, and is actually a question.... it's unclear what you're trying to ask. Perhaps you can edit to make the question more clear.Cece
Ok @Cece I understand your point. My aim was this. Thank you for your help.Maffa
@Cece I have updated the question, hopefully it's more clear now to me as well, thanks again.Maffa
I've answered a similar question here: https://mcmap.net/q/146414/-can-i-mix-uikit-and-tvmlkit-within-one-app/33531442#33531442Glossographer
If working with pure Swift, you can't use JSExportAs. Instead you can prepend @objc(shortJSname:) to function defs in both the JSExport prototype and class to give methods a different name in JS world. add a colon per argument (named or not)Goode
G
18

This WWDC Video explains how to communicate between JavaScript and Obj-C

Here is how I communicate from Swift to JavaScript:

//when pushAlertInJS() is called, pushAlert(title, description) will be called in JavaScript.
func pushAlertInJS(){
    
    //allows us to access the javascript context
    appController!.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in
        
        //get a handle on the "pushAlert" method that you've implemented in JavaScript
        let pushAlert = evaluation.objectForKeyedSubscript("pushAlert")
        
        //Call your JavaScript method with an array of arguments
        pushAlert.callWithArguments(["Login Failed", "Incorrect Username or Password"])
        
        }, completion: {(Bool) -> Void in
        //evaluation block finished running
    })
}

Here is how I communicate from JavaScript to Swift (it requires some setup in Swift):

//call this method once after setting up your appController.
func createSwiftPrint(){

//allows us to access the javascript context
appController?.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in

    //this is the block that will be called when javascript calls swiftPrint(str)
    let swiftPrintBlock : @convention(block) (String) -> Void = {
        (str : String) -> Void in

        //prints the string passed in from javascript
        print(str)
    }

    //this creates a function in the javascript context called "swiftPrint". 
    //calling swiftPrint(str) in javascript will call the block we created above.
    evaluation.setObject(unsafeBitCast(swiftPrintBlock, AnyObject.self), forKeyedSubscript: "swiftPrint" as (NSCopying & NSObjectProtocol)?)
    }, completion: {(Bool) -> Void in
    //evaluation block finished running
})
}

[UPDATE] For those of you who would like to know what "pushAlert" would look like on the javascript side, I'll share an example implemented in application.js

var pushAlert = function(title, description){
   var alert = createAlert(title, description);
   alert.addEventListener("select", Presenter.load.bind(Presenter));
   navigationDocument.pushDocument(alert);
}


// This convenience funnction returns an alert template, which can be used to present errors to the user.

var createAlert = function(title, description) {  

   var alertString = `<?xml version="1.0" encoding="UTF-8" ?>
       <document>
         <alertTemplate>
           <title>${title}</title>
           <description>${description}</description>

         </alertTemplate>
       </document>`

   var parser = new DOMParser();

   var alertDoc = parser.parseFromString(alertString, "application/xml");

   return alertDoc
}
Glossographer answered 6/11, 2015 at 18:58 Comment(11)
Could you please post how the javascript code looks for "pushAlert". Also is it in your presenter.js?Worried
@ChrisBrasino If you are using Apple's Catalog example with application.js, presenter.js, & resourceloader.js, I would put the declaration of "pushAlert" in application.js.Glossographer
Thanks will give it a shot!Worried
Attempted adding pushAlert to application.js with no luck. Crashes the build. Have you tried this with Apple's Catalog? Im just thinking it might not be possible.Worried
I can assure you that it is possible, I have done it. Can you post a separate question with your specific code? I will try to answer it.Glossographer
Thanks that would be awesome. Here is the question: #34190550Worried
@ChrisBrasino I've updated my answer to include the JS code, this may help you.Glossographer
@amok: My answer addresses your question. pushAlert.callWithArguments(["Login Failed", "Incorrect Username or Password"]) will call the function in javascript with 2 arguments. The method callWithArguments takes an array of arguments and passes them to the associated javascript method you've implemented.Glossographer
Could you please give me some idea for the InApp Purchase in the TVOS with TVML Kit. Its good if you provide some sample code for this. Thanks in advance :)Lager
As of 2021, in createSwiftPrint, we need setObject to: evaluation.setObject(unsafeBitCast(swiftPrintBlock, to: AnyObject.self), forKeyedSubscript: "swiftPrint" as (NSCopying & NSObjectProtocol)?) to avoid "ambiguous without more context"Gyve
Followed the instructions and placed the swiftPrint function in the app.js, and got "cannot find variable swiftPrint". Couldn't tell if I'm doing something wrong, or this doesn't work. Would appreciate some help.Imprint
I
0

You sparked an idea that worked...almost. Once you have displayed a native view, there is no straightforward method as-of-yet to push an TVML-based view onto the navigation stack. What I have done at this time is:

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.appController?.navigationController.popViewControllerAnimated(true)
dispatch_async(dispatch_get_main_queue()) {
    tvmlContext!.evaluateScript("showTVMLView()")
}

...then on the JavaScript side:

function showTVMLView() {setTimeout(function(){_showTVMLView();}, 100);}
function _showTVMLView() {//push the next document onto the stack}

This seems to be the cleanest way to move execution off the main thread and onto the JSVirtualMachine thread and avoid the UI lockup. Notice that I had to pop at the very least the current native view controller, as it was getting sent a deadly selector otherwise.

Incongruous answered 27/10, 2015 at 5:34 Comment(1)
So you moved the setTimeout callback within the JavaScriptCore virtual machine, instead of calling it from the native counterpart like I did above. Right, I think that's a good option. Why evaluateScript and not callWithArguments?Maffa

© 2022 - 2024 — McMap. All rights reserved.