Possible to call a Swift/ObjC function from TVJS?

I was wondering if any mechanism exists to essentially call a Swift/ObjC function from TVJS? I have some functionality that can't be done in JavaScript (due to CORS restrictions) and was hoping I would still be able to use TVML/TVJS instead of having to switch the whole app to Swift/ObjC.

Accepted Reply

You should use TVApplicationControllerDelegate


func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext){

let debug : @convention(block) (NSString!) -> Void = {

(string : NSString!) -> Void in

#if DEBUG

print("[log]: \(string)\n")

#endif

}

jsContext.setObject(unsafeBitCast(debug, AnyObject.self), forKeyedSubscript: "debug")

}



and in js code: debug('Hello world')

Replies

You should use TVApplicationControllerDelegate


func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext){

let debug : @convention(block) (NSString!) -> Void = {

(string : NSString!) -> Void in

#if DEBUG

print("[log]: \(string)\n")

#endif

}

jsContext.setObject(unsafeBitCast(debug, AnyObject.self), forKeyedSubscript: "debug")

}



and in js code: debug('Hello world')

Thanks for this. Also, the swift code that needs to execute is asynchronous, so is it possible to take a success and failure function as parameters and then call those functions from swift? So, in javascript, the function that I'm trying to write in swift would be like:


function myfunc(success, failure) {
     var request = new XMLHttpRequest()
     request.onreadystatechange = function() {
          if (request.readyState == 4) {
               if (request.status == 200) {
                    success(someVar);
               } else {
                    failure(someVar);
               }
          }
     }
     ...
     request.send();
}

So after further investigation, there is an evaluateScript method on jsContext that executes a javascript string. I can pass the names of global javascript functions as strings to my swift function, which can then execute those functions using evaluateScript. It's kinda *****, but it works. But please let me know if anyone has any better solutions.

I copied this code and also pasted the debug('Hello world') into my Presenter.js but nothing happens.


Is there a chance to see a working javascript and swift code please?

So, the rule is "keep it simple"


This is a simple wrapper class in your Swift project (actually I prefer ObjC and there is a reason for that...)



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)")

}

}


Then in your 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) {

print("\(__FUNCTION__) invoked with error: \(error)")


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]?) {

print("\(__FUNCTION__) invoked with options: \(options)")

}


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

print("\(__FUNCTION__) invoked with options: \(options)")

}


}


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 Launch

*/

App.onLaunch = function(options) {

console.log( options )


console.log( objectwrapper )


// here just access object methods as defined in the JSExport protocol
var text = objectwrapper.getItem()
console.log( text )


// 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 Exit

*/

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;




You can see a JSValue* passed in. This is the way to pass Completion handlers between ObjC ( Swift also works) and JavaScript. You just have to define in the javascript the function with the methods passed in like in the call defined before:


var networkChanged = function(status) {

console.log("Network changed " + status)

}



and the 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]];

});




Consider that you need to dispatch on the main thread and async, since you will hang the JavaScript TVML User-Interface otherways.
Look at the javascript "setTimeout" call here, and the call to the completion handler 'networkChanged' that has a javascript counter part with a integer value as parameter.




So, to recap:


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

Thank you so much for this detailed explanation!

I will try to make a complete run-through later!


Currently I am working with this neat snippet to get values from JS to Swift which is working like a charm:


https://gist.github.com/rayh/3d8382bb7313f6035c6e


In JS I write "commandXY" into the console and read ithat message in Swift.

And then If (message == "commandXY") ... dosomething() ...

Hi all,


Have been using the snipets provided above by everyone and have come close to a working solution that bridges JS->Swift->JS. But have run into a problem. Hope you can help.


Goal:

- Call Swift func from JS with completion handler.

- Execute the completion handler from Swift after some long running task.


JS:

externalCode.mySwiftFunc(function() {
     var alert = createAlert("myAlert");
     navigationDocument.presentModal(alert);
});


Swift:

func mySwiftFunc(completion: JSValue) -> Void {
     self.aLongRunningTask(completion: {(result: Dictionary<String, AnyObject>) -> Void in
        completion.callWithArguments([])
    })
}


- If I remove the alert creation JS code, everything works perfectly - I get a callback from JS after the Swift task has completed.

- The moment I put the alert code back in, I get a Swift BAD_ACCESS error and the app crashes.

- If I include the alert code, but move completion.callWithAttributes([]) out of the long running task to return immediately, everything works perfectly and the alert shows fine.


I've debugged the alert code down to the following line in the boilerplate application.js:createAlert() code:

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


And specficially the contents of alertString. The moment alertString contains a node such as "<document>", the app crashes. If the alertString is just something like `<?xml version="1.0" encoding="UTF-8" ?>`, then the code works fine.


Any help greatly appreciated here. Why would this code crash when callWithArguments is called only from an internal completion block? And why would this depend on the contents of the alertString when rendering an alert in JS?


Thanks,

I strongly suggest you to do the following


[self.callback.context[@"setTimeout"]
             callWithArguments:@[callback, @0, items]];



when you are going to send response to the JavaScriptCore counterpart. This will prevent the TVML UI MainThread to hang. As you can see it's a call of the setTimeout javascript function with delay 0, your callback and items as parameters like:


setTimeout(callback,0,items)


I'm not sure how you are creating the alert anyways here is one from Apple:


createAlert : function(title, description) {
  var alertString = `<?xml version="1.0" encoding="UTF-8" ?>
  <document>
  <alertTemplate>
  <title>${title}</title>
  <description>${description}</description>
  <button class="btn_close">
  <text>OK</text>
  </button>
  </alertTemplate>
  </document>`
  var parser = new DOMParser();
  var alertDoc = parser.parseFromString(alertString, "application/xml");
  return alertDoc
  }


There is no direct relationship with the alert and the behavior you are seeing here, it's more a side effect of calling this


completion.callWithArguments([])


in a unexpected way. It's better that you save your completion somewhere, and get a reference to it on the object instance. Then, when the long task ends, you call it. Also if you are performing a long task, it's reasonable that you move everything in a NSOperation like this:



/** JavaScriptCore Callback Operation */
@interface JSCallbackOperation: NSOperation
@property(nonatomic, strong) JSValue*callback;
@property(nonatomic, strong) id items;
@end
@implementation JSCallbackOperation
- (id)initWithItems:(id)items callback:(JSValue*)callback {
    if(self = [super init]) {
        self.items=items;
        self.callback=callback;
    }
    return self;
}
- (void)main {
    @autoreleasepool {
        if(self.callback) {
            NSLog(@"Dispatching %@", self.callback);
            [self.callback.context[@"setTimeout"]
             callWithArguments:@[self.callback, @0, self.items]];
        }
    }
}



At this point you define a helper a call the callbacks with parameters then:



#pragma mark - API Helper
- (void)handleResponseWithItems:(id)items callback:(JSValue*)callback {
   
    NSArray *active_and_pending_operations = operationQueue.operations;
    NSInteger count_of_operations = operationQueue.operationCount;
   
    NSLog(@"Running operations: %ld of %ld", active_and_pending_operations.count, count_of_operations);
   
    JSCallbackOperation *op = [[JSCallbackOperation alloc] initWithItems:items callback:callback];
    [op setQueuePriority:NSOperationQueuePriorityNormal];
    [op setCompletionBlock:^{
        NSLog(@"Operation completed.");
    }];
    [operationQueue addOperation:op];
   
}

Thanks so much for your help!


As you pointed out, it looks this was a threading issue. I'm not sure of the exact cause or reason so can't provide much more detail for anyone else's gain, but never-the-less, here's the Swift port of your solution that's working for me:


func mySwiftFunc(completion: JSValue) -> Void {
     let completionWrapper = JSContext.currentContext().objectForKeyedSubscript("setTimeout")

     self.aLongRunningTask(completion: {(result: Dictionary<String, AnyObject>) -> Void in
          completionWrapper.callWithArguments([completion, 0, paramsPassedToCompletion])
     })
}


As you say, this could be improved by utilizing NSOperation and storing a reference to completion.


Thanks again.