Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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
like image 755
loretoparisi Avatar asked Oct 12 '15 12:10

loretoparisi


1 Answers

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
}
like image 50
shirefriendship Avatar answered Nov 28 '22 08:11

shirefriendship