Подтвердить что ты не робот

Как связать 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
4b9b3361

Ответ 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 и избежать блокировки пользовательского интерфейса. Обратите внимание, что мне приходилось по крайней мере набирать текущий контроллер представления, поскольку в противном случае он отправил смертельный селектор.