Как соединить TVML/JavaScriptCore с UIKit/Objective-C (Swift)?

На данный момент tvOS поддерживает два способа создания телевизионных приложений, TVML и UIKit, и нет официальных упоминаний о том, как смешивать вещи, чтобы создать пользовательский интерфейс TVML (то есть в основном XML) с родной частью для логики приложения, и я /O (например, воспроизведение, потоковая передача, сохранение 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

person loretoparisi    schedule 12.10.2015    source источник
comment
Это не вопрос, насколько я могу судить. Если вы нашли полезную информацию, которой хотите поделиться, задайте свой вопрос и ответьте на него. Кроме того, я думаю, что эта тема поднималась где-то в tvos или apple-tvos уже есть, поэтому вам, вероятно, не нужно задавать новый вопрос, просто ответьте на существующий.   -  person rickster    schedule 26.10.2015
comment
@ricksterЕсли бы я нашел ответ на этот вопрос, то ответил бы на него, но пока нет. Конкретного вопроса о tvOS и TVML + UIKIT нет, поэтому я не понимаю вашу точку зрения. Да, возможно вопрос не ясен, и я мог бы уточнить. Ваш ответ не является конструктивным, поскольку tvOS — это совершенно новая технология, в которой мало знаний о Stackoverflow. Конечно, это моя точка зрения, я уверен, что вопрос в любом случае конструктивен.   -  person loretoparisi    schedule 26.10.2015
comment
Если эта публикация не является попыткой предоставить информацию, а на самом деле является вопросом... непонятно, что вы пытаетесь спросить. Возможно, вы можете отредактировать, чтобы сделать вопрос более ясным.   -  person rickster    schedule 26.10.2015
comment
Хорошо, @rickster, я понимаю твою точку зрения. Моя цель была в этом. Спасибо за помощь.   -  person loretoparisi    schedule 26.10.2015
comment
@rickster Я обновил вопрос, надеюсь, теперь и мне стало понятнее, еще раз спасибо.   -  person loretoparisi    schedule 26.10.2015
comment
Я ответил на аналогичный вопрос здесь: http://stackoverflow.com/questions/33305352/can-i-mix-uikit-and-tvmlkit-within-one-app/33531442#33531442   -  person shirefriendship    schedule 06.11.2015
comment
Если вы работаете с чистым Swift, вы не можете использовать JSExportAs. Вместо этого вы можете добавить @objc(shortJSname:) перед определениями функций как в прототипе JSExport, так и в классе, чтобы дать методам другое имя в мире JS. добавить двоеточие для каждого аргумента (с именем или без)   -  person kfix    schedule 30.01.2016


Ответы (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" as (NSCopying & NSObjectProtocol)?)
    }, 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
}
person shirefriendship    schedule 06.11.2015
comment
Не могли бы вы опубликовать, как выглядит код javascript для pushAlert. Также это есть в вашем Presenter.js? - person Chris Brasino; 08.12.2015
comment
@ChrisBrasino Если вы используете пример каталога Apple с application.js, Presenter.js и resourceloader.js, я бы поместил объявление pushAlert в application.js. - person shirefriendship; 09.12.2015
comment
Спасибо попробую! - person Chris Brasino; 09.12.2015
comment
Попытка добавления pushAlert в application.js не удалась. Вылетает сборка. Вы пробовали это с Каталогом Apple? Я просто думаю, что это может быть невозможно. - person Chris Brasino; 10.12.2015
comment
Я могу заверить вас, что это возможно, я сделал это. Можете ли вы опубликовать отдельный вопрос с вашим конкретным кодом? Я постараюсь ответить на него. - person shirefriendship; 10.12.2015
comment
Спасибо, это было бы здорово. Вот вопрос: stackoverflow.com/questions/34190050/ - person Chris Brasino; 10.12.2015
comment
@ChrisBrasino Я обновил свой ответ, включив в него код JS, это может вам помочь. - person shirefriendship; 10.12.2015
comment
@amok: мой ответ касается вашего вопроса. pushAlert.callWithArguments([Ошибка входа, Неверное имя пользователя или пароль]) вызовет функцию в javascript с двумя аргументами. Метод callWithArguments принимает массив аргументов и передает их связанному реализованному вами методу javascript. - person shirefriendship; 16.02.2016
comment
Не могли бы вы дать мне некоторое представление о покупке InApp в TVOS с комплектом TVML. Хорошо, если вы предоставите пример кода для этого. Заранее спасибо :) - person Purushottam Padhya; 21.07.2016
comment
Начиная с 2021 года, в createSwiftPrint нам нужно setObject to: Assessment.setObject(unsafeBitCast(swiftPrintBlock, to: AnyObject.self), forKeyedSubscript: swiftPrint as (NSCopying & NSObjectProtocol)?), чтобы избежать двусмысленности без дополнительного контекста. - person PhoenixB; 02.02.2021
comment
Выполнил инструкции и поместил функцию swiftPrint в app.js, но не смог найти переменную swiftPrint. Не могу сказать, то ли я делаю что-то не так, то ли это не работает. Был бы признателен за помощь. - person Andres Urdaneta; 22.02.2021

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

person marcospolanco    schedule 27.10.2015
comment
Таким образом, вы переместили обратный вызов setTimeout в виртуальную машину JavaScriptCore вместо того, чтобы вызывать его из собственного аналога, как я сделал выше. Да, я думаю, это хороший вариант. Почему evaluateScript, а не callWithArguments? - person loretoparisi; 27.10.2015