【问题标题】:How to bridge TVML/JavaScriptCore to UIKit/Objective-C (Swift)?如何将 TVML/JavaScriptCore 连接到 UIKit/Objective-C (Swift)?
【发布时间】:2016-01-09 22:59:49
【问题描述】:

到目前为止tvOS 支持两种制作电视应用程序的方法,TVML 和 UIKit,并且没有官方提到如何将东西混合以制作 TVML(基本上是 XML)用户界面与本机对应部分应用逻辑和 I/O(如播放、流式传输、iCloud 持久性等)。

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

在下文中,我尝试了一个解决方案,该解决方案遵循从 Apple 论坛改编的代码 sn-ps 以及有关 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 Counter部分你不会做驼峰式的:

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]];
                                       });

在我的发现中,我发现如果您不在主线程和异步上调度,MainThread 将挂起。所以我将调用调用完成处理程序回调的javascript“setTimeout”调用。

所以我在这里使用的方法是:

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

[更新] 为了更清楚,我已经更新了这个问题。我正在寻找一种桥接TVMLUIKit 的技术解决方案,以便

  • 通过JavaScriptCode了解最佳编程模型
  • 拥有从JavaScriptCoreObjectiveC 和 反之亦然
  • Objective-C 调用JavaScriptCode 时表现最佳

【问题讨论】:

  • 据我所知,这不是一个问题。如果您发现了一些想要分享的有用信息,ask and answer your own question。另外,我认为这个话题已经在 tvosapple-tvos 的某个地方提出来了,所以你可能不需要问一个新问题,只需回答一个现有的问题。
  • @rickster如果我能找到这个问题的答案,我会回答的,但到目前为止还没有。没有关于tvOSTVML + UIKIT 的具体问题,所以我不明白你的意思。是的,也许这个问题不清楚,我可以更好地说明。您的回答是“不具建设性的”,因为tvOS 是一种全新的技术,对 Stackoverflow 知之甚少。当然这是我的观点,我很确定这个问题无论如何都是“建设性的”。
  • 如果这个帖子不是试图提供信息,实际上是一个问题......不清楚你想问什么。也许您可以编辑以使问题更清楚。
  • 好的@rickster 我明白你的意思。我的目标是这个。感谢您的帮助。
  • @rickster 我已经更新了这个问题,希望现在对我来说也更清楚了,再次感谢。

标签: objective-c tvos javascriptcore


【解决方案1】:

这个WWDC Video 解释了如何在 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
}

【讨论】:

  • 能否请您发布 javascript 代码如何查找“pushAlert”。它也在你的presenter.js中吗?
  • @ChrisBrasino 如果您将 Apple 的 Catalog 示例与 application.js、presenter.js 和 resourceloader.js 一起使用,我会将“pushAlert”的声明放在 application.js 中。
  • @ChrisBrasino 我已经更新了我的答案以包含 JS 代码,这可能会对您有所帮助。
  • @amok:我的回答解决了你的问题。 pushAlert.callWithArguments(["Login Failed", "Incorrect Username or Password"]) 将在 javascript 中使用 2 个参数调用该函数。 callWithArguments 方法接受一个参数数组并将它们传递给您已实现的相关 javascript 方法。
  • 截至 2021 年,在 createSwiftPrint 中,我们需要 setObject to: evaluation.setObject(unsafeBitCast(swiftPrintBlock, to: AnyObject.self), forKeyedSubscript: "swiftPrint" as (NSCopying & NSObjectProtocol)?) 来避免“没有更多上下文的模棱两可”
【解决方案2】:

你激发了一个可行的想法......几乎。一旦显示了原生视图,目前还没有直接的方法可以将基于 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 锁定的最干净的方法。请注意,我至少必须弹出当前的原生视图控制器,否则它会收到一个致命的选择器。

【讨论】:

  • 所以您在 JavaScriptCore 虚拟机中移动了 setTimeout 回调,而不是像我在上面所做的那样从本机对应物调用它。是的,我认为这是一个不错的选择。为什么是evaluateScript 而不是callWithArguments
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多