如何桥接TVML/JavaScriptCore到UIKit/Objective-C (Swift)



到目前为止,tvOS支持两种方法来制作电视应用程序,TVML和UIKit,并且没有官方提到如何将事物混合起来制作TVML(基本上是XML)用户界面与应用程序逻辑和I/O(如播放,流媒体,iCloud持久化等)的本地counter部分。

那么,在新的tvOS应用程序中混合TVMLUIKit的最佳解决方案是什么?

在下面我已经尝试了一个解决方案以下代码片段改编自苹果论坛和有关JavaScriptCore到ObjC/Swift绑定的相关问题。这是你的Swift项目中的一个简单的包装类。

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

,委托必须符合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]?) {
    }
}

此时,javascript非常简单,就像:看一下带有命名参数的方法,你需要改变javascript计数器部分的方法名:

   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');
    }
现在,假设你有一个非常复杂的js接口要导出,比如
@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";
}

你可以这样做

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

此时,在JS计数器部分,您将不会执行驼峰情况:

objectwrapper.bootNetworkUser(statusChanged,networkChanged,userChanged)

但你要做的是:

objectwrapper.boot(statusChanged,networkChanged,userChanged)

最后,再看一下这个接口:

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

传入的值JSValue*。是在ObjC/SwiftJavaScriptCore之间传递完成处理程序的一种方法。此时,在本机代码中,所有调用都带参数:

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

在我的发现,我已经看到,主线程将挂起,如果你不调度主线程和异步。因此,我将调用javascript的"setTimeout"调用,它调用完成处理程序回调。

这里我使用的方法是:

  • 使用JSExportAs获取带有命名参数的方法,避免使用驼驼式javascript对应的callMyParam1Param2Param3
  • 使用JSValue作为参数来摆脱完成处理程序。在本机端使用callWithArguments。在JS端使用javascript函数;
  • dispatch_async用于完成处理程序,可能在JavaScript端调用setTimeout 0-delayed,以避免UI冻结。
(更新)

我对这个问题做了更新,以便更清楚地说明。我正在寻找桥接TVMLUIKit的技术解决方案,以便

  • 了解JavaScriptCode的最佳编程模型
  • JavaScriptCoreObjectiveC之间有正确的桥接反之亦然
  • Objective-C调用JavaScriptCode时具有最佳性能

这个WWDC视频解释了如何在JavaScript和Obj-C之间进行通信

这是我如何从Swift到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
    })
}

这是我如何从JavaScript到Swift的通信(它需要在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]对于那些想知道什么"pushAlert"在javascript方面,我将分享一个在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
}

你的想法几乎奏效了。显示本机视图之后,目前还没有直接的方法将基于tvml的视图推到导航堆栈上。此时我所做的是:

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

…然后在JavaScript端:

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

这似乎是将执行从主线程转移到JSVirtualMachine线程并避免UI锁定的最干净的方法。注意,我至少需要弹出当前本机视图控制器,因为它会收到一个致命的选择器。

最新更新