我可以在一个应用程序中混合使用UIKit和TVMLKit吗?

Raf*_*fAl 17 uikit apple-tv tvos tvml

我在探索tvOS,我发现,苹果提供了很好的一套模板使用写入TVML.我想知道tvOS使用TVML模板的应用程序是否也可以使用UIKit.

我可以在一个应用程序中混合使用UIKit和TVMLKit吗?

我在Apple开发者论坛上发现了一个帖子,但它没有完全回答这个问题,我正在通过文档找到答案.

shi*_*hip 33

是的你可以.显示TVML模板需要您使用控制JavaScript上下文的对象:TVApplicationController.

var appController: TVApplicationController?
Run Code Online (Sandbox Code Playgroud)

该对象具有与之关联的UINavigationController属性.所以无论何时你认为合适,你都可以打电话:

let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)
Run Code Online (Sandbox Code Playgroud)

这允许您将Custom UIKit视图控制器推送到导航堆栈.如果你想回到TVML模板,只需将viewController从导航堆栈中弹出即可.

如果你想知道如何在JavaScript和Swift之间进行通信,这里有一个创建一个名为pushMyView()的javascript函数的方法

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
    })
}
Run Code Online (Sandbox Code Playgroud)

一旦你调用createPushMyView()在斯威夫特,你可以自由地调用pushMyView()在你的JavaScript代码,它会推视图控制器压入堆栈.

SWIFT 4.1更新

只需对方法名称和转换进行一些简单的更改:

appController?.evaluate(inJavaScriptContext: {(evaluation: JSContext) -> Void in
Run Code Online (Sandbox Code Playgroud)

evaluation.setObject(unsafeBitCast(pushMyViewBlock, to: AnyObject.self), forKeyedSubscript: "pushMyView" as NSString)
Run Code Online (Sandbox Code Playgroud)

  • 太棒了!这回答的问题多于所要求的问题,例如JS和Swift在这种情况下的互操作.似乎应该在调用application.js之前注入所有javascript函数(例如此示例).公平? (2认同)

Jas*_*ell 6

如接受的答案中所述,您可以在JavaScript上下文中调用任何Swift函数.请注意,顾名思义,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]
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在你的app控制器中:

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

然后你可以在你的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")
Run Code Online (Sandbox Code Playgroud)

快速自定义元素示例:

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)?) {
        //...
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是如何设置自定义界面工厂:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,不会为所有元素调用view/viewControllerForElement :. 一些现有元素(如集合视图)将处理其子元素本身的呈现,而不涉及您的接口工厂,这意味着您必须覆盖更高级别的元素,或者可能使用类别/混合或UIAppearance来获取你想要的效果.

最后,正如我刚才暗示的那样,您可以使用UIAppearance来更改某些内置视图的外观.这是更改TVML应用程序标签栏外观的最简单方法,例如:

 // in didFinishLaunching...
 UITabBar.appearance().backgroundImage = UIImage()
 UITabBar.appearance().backgroundColor = UIColor(white: 0.5, alpha: 1.0)
Run Code Online (Sandbox Code Playgroud)


Han*_*son 5

如果你已经有一个用于 tvOS 的原生 UIKit 应用程序,但想通过使用 TVMLKit 对其进行扩展,你可以。

在您的原生 tvOS 应用程序中使用 TVMLKit 作为子应用程序。下面的程序演示如何做到这一点,通过保留TVApplicationController和呈现navigationControllerTVApplicationController。将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)
    }
Run Code Online (Sandbox Code Playgroud)