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
Daniel Storm
  • 18,301
  • 9
  • 84
  • 152
loretoparisi
  • 15,724
  • 11
  • 102
  • 146
  • 1
    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](http://stackoverflow.com/help/self-answer). Also, I think this topic has ben brought up somewhere in [tag:tvos] or [tag:apple-tvos] already, so you probably don't need to ask a new question, just answer an existing one. – rickster Oct 26 '15 at 15:13
  • 1
    @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. – loretoparisi Oct 26 '15 at 15:18
  • 3
    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. – rickster Oct 26 '15 at 15:25
  • Ok @rickster I understand your point. My aim was this. Thank you for your help. – loretoparisi Oct 26 '15 at 15:26
  • @rickster I have updated the question, hopefully it's more clear now to me as well, thanks again. – loretoparisi Oct 26 '15 at 15:43
  • I've answered a similar question here: [http://stackoverflow.com/questions/33305352/can-i-mix-uikit-and-tvmlkit-within-one-app/33531442#33531442](http://stackoverflow.com/questions/33305352/can-i-mix-uikit-and-tvmlkit-within-one-app/33531442#33531442) – shirefriendship Nov 05 '15 at 21:05
  • 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) – kfix Jan 30 '16 at 20:35

2 Answers2

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
}
PhoenixB
  • 398
  • 1
  • 3
  • 12
shirefriendship
  • 1,293
  • 8
  • 18
  • Could you please post how the javascript code looks for "pushAlert". Also is it in your presenter.js? – Chris Brasino Dec 08 '15 at 20:29
  • @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. – shirefriendship Dec 09 '15 at 04:05
  • Thanks will give it a shot! – Chris Brasino Dec 09 '15 at 18:57
  • 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. – Chris Brasino Dec 09 '15 at 21:19
  • 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. – shirefriendship Dec 09 '15 at 22:02
  • Thanks that would be awesome. Here is the question: http://stackoverflow.com/questions/34190050/how-to-call-tvjs-method-from-native-objective-c – Chris Brasino Dec 09 '15 at 22:18
  • 1
    @ChrisBrasino I've updated my answer to include the JS code, this may help you. – shirefriendship Dec 09 '15 at 22:18
  • 1
    @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. – shirefriendship Feb 16 '16 at 04:58
  • 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 :) – Purushottam Padhya Jul 21 '16 at 03:45
  • 1
    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" – PhoenixB Feb 01 '21 at 23:57
  • 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. – Andres Urdaneta Feb 22 '21 at 19:12
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.

marcospolanco
  • 156
  • 1
  • 5
  • 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```? – loretoparisi Oct 27 '15 at 11:48