Как связать 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
использовать множество методов с именованными параметрами и избегать использования JavaScript, таких как callMyParam1Param2Param3 - использование
JSValue
в качестве параметра, чтобы избавиться от обработчиков завершения. Используйте callWithArguments на родной стороне. Используйте функции JavaScript на стороне JS; dispatch_async
для обработчиков завершения, возможно вызывающих setTimeout 0-delayed на стороне JavaScript, чтобы избежать зависания пользовательского интерфейса.
[ОБНОВЛЕНИЕ] Я обновил этот вопрос, чтобы быть более ясным. Я нахожу техническое решение для преодоления TVML
а также UIKit
чтобы
- Понять лучшую модель программирования с
JavaScriptCode
- Иметь правильный мост от
JavaScriptCore
вObjectiveC
и наоборот - Иметь лучшие выступления при звонке
JavaScriptCode
отObjective-C
2 ответа
Это видео 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")
}, completion: {(Bool) -> Void in
//evaluation block finished running
})
}
[ОБНОВЛЕНИЕ] Для тех из вас, кто хотел бы знать, как выглядит "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 и избежать блокировки пользовательского интерфейса. Заметьте, что мне нужно было вытолкнуть, по крайней мере, текущий контроллер нативного представления, так как иначе он получал смертельный селектор.