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

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

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

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

4 ответов


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

var appController: TVApplicationController?

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

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

Это позволяет вам нажать пользовательский UIKit viewcontroller в стек навигации. Если вы хотите вернуться к шаблонам 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, и он будет толкать контроллер представления в стек.

SWIFT 4.1 ОБНОВЛЕНИЕ

всего несколько простые изменения в именах методов и кастинге:

appController?.evaluate(inJavaScriptContext: {(evaluation: JSContext) -> Void in

и

evaluation.setObject(unsafeBitCast(pushMyViewBlock, to: AnyObject.self), forKeyedSubscript: "pushMyView" as NSString)

как упоминалось в принятом ответе, вы можете вызвать практически любую функцию 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]
    }
}

затем в вашем App controller:

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's 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's 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: не будет вызываться для всех элементов. Некоторые из существующих элементов (например, представления коллекции) будут обрабатывать рендеринг самих дочерних элементов без участия фабрики интерфейса, что означает, что вам придется переопределить элемент более высокого уровня или, возможно, использовать категорию/swizzling или UIAppearance, чтобы получить желаемый эффект.

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

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

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

используйте TVMLKit в качестве приложения sub в вашем родном приложении tvOS. Следующее приложение показывает, как это сделать, сохранив TVApplicationController и вручить navigationController С TVApplicationController. The 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)
    }

да. Вижу Основы TVMLKit, чьи документы начинаются с:

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

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