Как связать TVML/JavaScriptCore с UIKit/Objective-C (Swift)?
Пока tvOS
поддерживает два способа создания ТВ-приложений, TVML и UIKit, и нет официальных упоминаний о том, как смешивать вещи, чтобы сделать TVML (это в основном XML). Пользовательский интерфейс с собственной частью счетчика для логика приложения и ввод-вывод (например, воспроизведение, потоковая передача, постоянство iCloud и т.д.).
Итак, это лучшее решение для смешивания TVML
и UIKit
в новом tvOS
приложении?
В следующем примере я пробовал решение после фрагментов кода, адаптированных из форумов Apple, и связанных с ними вопросов о привязке 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/Swift
и JavaScriptCore
. В этот момент в нативном коде вы все вызываете с аргументами:
dispatch_async(dispatch_get_main_queue(), ^{
NSNumber *state = [NSNumber numberWithInteger:status];
[networkChanged.context[@"setTimeout"]
callWithArguments:@[networkChanged, @0, state]];
});
В моих выводах я видел, что MainThread будет зависать, если вы не отправляете основной поток и асинхронный. Поэтому я буду называть javascript "setTimeout", вызывающий обратный вызов обработчика завершения.
Итак, подход, который я использовал здесь:
- Используйте
JSExportAs
, чтобы взять автомобиль методов с именованными параметрами и избегать похожих на java-скрипт верблюда, таких как callMyParam1Param2Param3
- Используйте
JSValue
как параметр, чтобы избавиться от обработчиков завершения. Используйте callWithArguments с внутренней стороны. Используйте javascript-функции на стороне JS;
-
dispatch_async
для обработчиков завершения, возможно, вызвав setTimeout с задержкой 0 на стороне JavaScript, чтобы избежать зависания пользовательского интерфейса.
[ОБНОВЛЕНИЕ]
Я уточнил этот вопрос, чтобы быть более ясным. Я нахожу техническое решение для моста TVML
и UIKit
, чтобы
- Понять лучшую модель программирования с помощью
JavaScriptCode
- Имеем правильный мост от
JavaScriptCore
до ObjectiveC
и
viceversa
- Получите лучшие результаты при вызове
JavaScriptCode
из Objective-C
Ответы
Ответ 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")
}, 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
}
Ответ 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 и избежать блокировки пользовательского интерфейса. Обратите внимание, что мне приходилось по крайней мере набирать текущий контроллер представления, поскольку в противном случае он отправил смертельный селектор.