Могу ли я смешивать UIKit и TVMLKit в одном приложении?

Я изучаю tvOS, и я обнаружил, что Apple предлагает хороший набор шаблонов, написанных с помощью TVML. Я хотел бы знать, может ли приложение tvOS использовать шаблоны TVML использовать UIKit.

Можно ли смешивать UIKit и TVMLKit в одном приложении?

Я нашел нить на форуме разработчиков Apple, но он не полностью отвечает на этот вопрос, и я просматриваю документацию, чтобы найти ответ.

Ответ 1

Да, вы можете. Для отображения шаблонов TVML вам необходимо использовать объект, который управляет контекстом JavaScript: TVApplicationController.

var appController: TVApplicationController?

Этот объект имеет свойство UINavigationController, связанное с ним. Поэтому, когда вы считаете нужным, вы можете позвонить:

let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)

Это позволяет вам нажимать настраиваемый диспетчер представлений UIKit на стек навигации. Если вы хотите вернуться к шаблонам TVML, просто вытащите viewController из стека навигации.

Если вы хотите узнать, как установить связь между JavaScript и Swift, вот метод, который создает функцию javascript под названием pushMyView()

func createPushMyView(){

    //allows us to access the javascript context
    appController?.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in

        //this is the block that will be called when javascript calls pushMyView()
        let pushMyViewBlock : @convention(block) () -> Void = {
            () -> Void in

            //pushes a UIKit view controller onto the navigation stack
            let myViewController = UIViewController()
            self.appController?.navigationController.pushViewController(myViewController, animated: true)
        }

        //this creates a function in the javascript context called "pushMyView". 
        //calling pushMyView() in javascript will call the block we created above.
        evaluation.setObject(unsafeBitCast(pushMyViewBlock, AnyObject.self), forKeyedSubscript: "pushMyView")
        }, completion: {(Bool) -> Void in
        //done running the script
    })
}

Как только вы вызываете createPushMyView() в Swift, вы можете называть pushMyView() в своем javascript-коде, и он будет выталкивать контроллер представления в стек.

Ответ 2

Как уже упоминалось в принятом ответе, вы можете вызвать практически любую функцию Swift из контекста JavaScript. Обратите внимание, что, как следует из названия, setObject:forKeyedSubscript: также будет принимать объекты (если они соответствуют протоколу, который наследует от JSExport) в дополнение к блокам, позволяя вам получать доступ к методам и свойствам этого объекта. Вот пример

import Foundation
import TVMLKit

// Just an example, use sessionStorage/localStorage JS object to actually accomplish something like this
@objc protocol JSBridgeProtocol : JSExport {
    func setValue(value: AnyObject?, forKey key: String)
    func valueForKey(key: String) -> AnyObject?
}

class JSBridge: NSObject, JSBridgeProtocol {
    var storage: Dictionary<String, String> = [:]
    override func setValue(value: AnyObject?, forKey key: String) {
        storage[key] = String(value)
    }
    override func valueForKey(key: String) -> AnyObject? {
        return storage[key]
    }
}

Затем в вашем контроллере приложения:

func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) {
    let bridge:JSBridge = JSBridge();
    jsContext.setObject(bridge, forKeyedSubscript:"bridge");
}

Тогда вы можете сделать это в своем JS: bridge.setValue(['foo', 'bar'], "baz")

Не только это, но вы можете переопределить представления для существующих элементов или определить пользовательские элементы для использования в своей разметке и вернуть их с собственными представлениями:

// Call lines like these before you instantiate your TVApplicationController 
TVInterfaceFactory.sharedInterfaceFactory().extendedInterfaceCreator = CustomInterfaceFactory() 
// optionally register a custom element. You could use this in your markup as <loadingIndicator></loadingIndicator> or <loadingIndicator /> with optional attributes. LoadingIndicatorElement needs to be a TVViewElement subclass, and there are three functions you can optionally override to trigger JS events or DOM updates
TVElementFactory.registerViewElementClass(LoadingIndicatorElement.self, forElementName: "loadingIndicator")

Пример быстрого пользовательского элемента:

import Foundation
import TVMLKit

class LoadingIndicatorElement: TVViewElement {
    override var elementName: String {
        return "loadingIndicator"
    }

    internal override func resetProperty(resettableProperty: TVElementResettableProperty) {
        super.resetProperty(resettableProperty)
    }
    // API to dispatch events to JavaScript
    internal override func dispatchEventOfType(type: TVElementEventType, canBubble: Bool, cancellable isCancellable: Bool, extraInfo: [String : AnyObject]?, completion: ((Bool, Bool) -> Void)?) {
        //super.dispatchEventOfType(type, canBubble: canBubble, cancellable: isCancellable, extraInfo: extraInfo, completion: completion)
    }

    internal override func dispatchEventWithName(eventName: String, canBubble: Bool, cancellable isCancellable: Bool, extraInfo: [String : AnyObject]?, completion: ((Bool, Bool) -> Void)?) {
        //...
    }
}

А вот как настроить пользовательский интерфейс factory:

class CustomInterfaceFactory: TVInterfaceFactory {
    let kCustomViewTag = 97142 // unlikely to collide
    override func viewForElement(element: TVViewElement, existingView: UIView?) -> UIView? {

        if (element.elementName == "title") {
            if (existingView != nil) {
                return existingView
            }

            let textElement = (element as! TVTextElement)
            if (textElement.attributedText!.length > 0) {
                let label = UILabel()                    

                // Configure your label here (this is a good way to set a custom font, for example)...  
                // You can examine textElement.style or textElement.textStyle to get the element style properties
                label.backgroundColor = UIColor.redColor()
                let existingText = NSMutableAttributedString(attributedString: textElement.attributedText!)
                label.text = existingText.string
                return label
            }
        } else if element.elementName == "loadingIndicator" {

            if (existingView != nil && existingView!.tag == kCustomViewTag) {
                return existingView
            }
            let view = UIImageView(image: UIImage(named: "loading.png"))
            return view // Simple example. You could easily use your own UIView subclass
        }

        return nil // Don't call super, return nil when you don't want to override anything... 
    }

    // Use either this or viewForElement for a given element, not both
    override func viewControllerForElement(element: TVViewElement, existingViewController: UIViewController?) -> UIViewController? {
        if (element.elementName == "whatever") {
            let whateverStoryboard = UIStoryboard(name: "Whatever", bundle: nil)
            let viewController = whateverStoryboard.instantiateInitialViewController()
            return viewController
        }
        return nil
    }


    // Use this to return a valid asset URL for resource:// links for badge/img src (not necessary if the referenced file is included in your bundle)
    // I believe you could use this to cache online resources (by replacing resource:// with http(s):// if a corresponding file doesn't exist (then starting an async download/save of the resource before returning the modified URL). Just return a file url for the version on disk if you've already cached it.
    override func URLForResource(resourceName: String) -> NSURL? {
        return nil
    }
}

К сожалению, view/viewControllerForElement: не будет вызываться для всех элементов. Некоторые из существующих элементов (например, представления коллекции) будут обрабатывать рендеринг самих дочерних элементов без привлечения вашего интерфейса factory, что означает, что вам придется переопределить элемент более высокого уровня или, возможно, использовать категорию /swizzling или UIAppearance чтобы получить эффект, который вы хотите.

Наконец, как я подразумевал, вы можете использовать UIAppearance, чтобы изменить способ отображения определенных встроенных представлений. Здесь самый простой способ изменить внешний вид панели вкладок приложения TVML, например:

 // in didFinishLaunching...
 UITabBar.appearance().backgroundImage = UIImage()
 UITabBar.appearance().backgroundColor = UIColor(white: 0.5, alpha: 1.0)

Ответ 3

Если у вас уже есть собственное приложение UIKit для tvOS, но вы хотите расширить его, используя TVMLKit для какой-то его части, вы можете.

Используйте TVMLKit в качестве дополнительного приложения в вашем родном приложении tvOS. В следующем приложении показано, как это сделать, сохраняя TVApplicationController и представляя navigationController из TVApplicationController. TVApplicationControllerContext используется для передачи данных в приложение JavaScript, поскольку URL-адрес передается здесь:

class ViewController: UIViewController, TVApplicationControllerDelegate {
    // Retain the applicationController
    var appController:TVApplicationController?
    static let tvBaseURL = "http://localhost:9001/"
    static let tvBootURL = "\(ViewController.tvBaseURL)/application.js"

    @IBAction func buttonPressed(_ sender: UIButton) {
        print("button")

        // Use TVMLKit to handle interface

        // Get the JS context and send it the url to use in the JS app
        let hostedContContext = TVApplicationControllerContext()
        if let url = URL(string:  ViewController.tvBootURL) {
            hostedContContext.javaScriptApplicationURL = url
        }

        // Save an instance to a new Sub application, the controller already knows what window we are running so pass nil
        appController = TVApplicationController(context: hostedContContext, window: nil, delegate: self)

        // Get the navigationController of the Sub App and present it
        let navc = appController!.navigationController
        present(navc, animated: true, completion: nil)
    }

Ответ 4

Да. См. TVMLKit Framework, чьи документы начинаются с:

Структура TVMLKit позволяет вам включать файлы JavaScript и TVML в ваши двоичные приложения для создания приложений клиент-сервер.

Из быстрого просмотра этих документов выглядит, что вы используете различные классы TVWhateverFactory для создания представлений UIKit или контроллеров представлений из TVML, после чего их можно вставлять в приложение UIKit.