我正在探索tvOS
,我发现Apple使用TVML
提供了很好的templates。我想知道使用tvOS
模板的TVML
应用是否也可以使用UIKit
。
我可以在一个应用中混合使用UIKit和TVMLKit吗?
我在Apple Developer Forum找到了一个帖子,但它没有完全回答这个问题,我正在通过文档找到答案。
答案 0 :(得分:31)
是的,你可以。显示TVML模板需要您使用控制JavaScript上下文的对象: TVApplicationController 。
var appController: TVApplicationController?
此对象具有与之关联的 UINavigationController 属性。所以无论何时你认为合适,你都可以打电话:
let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)
这允许您将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
})
}
在Swift中调用 createPushMyView()后,您可以在javascript代码中调用 pushMyView(),它会将视图控制器推送到堆栈中。< / p>
SWIFT 4.1更新
只需对方法名称和转换进行一些简单的更改:
appController?.evaluate(inJavaScriptContext: {(evaluation: JSContext) -> Void in
和
evaluation.setObject(unsafeBitCast(pushMyViewBlock, to: AnyObject.self), forKeyedSubscript: "pushMyView" as NSString)
答案 1 :(得分: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]
}
}
然后在您的app控制器中:
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)?) {
//...
}
}
以下是如何设置自定义界面工厂:
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 :.一些现有元素(如集合视图)将处理其子元素本身的呈现,而不涉及您的接口工厂,这意味着您必须覆盖更高级别的元素,或者可能使用类别/混合或UIAppearance来获取你想要的效果。
最后,正如我刚才暗示的那样,您可以使用UIAppearance来更改某些内置视图的外观。这是更改TVML应用标签栏外观的最简单方法,例如:
// in didFinishLaunching...
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().backgroundColor = UIColor(white: 0.5, alpha: 1.0)
答案 2 :(得分:2)
如果您已经有一个针对tvOS的原生UIKit应用程序,但想通过使用TVMLKit来扩展它的某些部分,你可以。
将TVMLKit用作原生tvOS应用中的子应用。以下应用通过保留TVApplicationController
并显示navigationController
中的TVApplicationController
来说明如何执行此操作。 TVApplicationControllerContext
用于将数据传输到JavaScript应用,因为网址传输到此处:
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)
}
答案 3 :(得分:-1)
是。请参阅TVMLKit Framework,其文档以:
开头TVMLKit框架使您能够在二进制应用程序中合并JavaScript和TVML文件,以创建客户端 - 服务器应用程序。
从这些文档的快速浏览中,看起来您使用各种TVWhateverFactory
类来创建UIKit视图或从TVML查看控制器,之后您可以将它们插入到UIKit应用程序中。