2015-10-23 29 views
17

Odkrywam tvOS i odkryłem, że Apple oferuje ładny zestaw templates napisany przy użyciu TVML. Chciałbym się dowiedzieć, czy aplikacja tvOS, która wykorzystuje szablony TVML, może również używać UIKit.Czy mogę mieszać UIKit i TVMLKit w ramach jednej aplikacji?

Czy mogę mieszać UIKit i TVMLKit w ramach jednej aplikacji?

Znalazłem wątek na Apple Developer Forum, ale nie w pełni odpowiada na to pytanie i przechodzę do dokumentacji, aby znaleźć odpowiedź.

Odpowiedz

31

Tak, możesz. Wyświetlanie szablonów TVML wymaga użycia obiektu, który kontroluje kontekst JavaScript: TVApplicationController.

var appController: TVApplicationController? 

Ten obiekt ma UINavigationController własności z nim związane. Jeśli więc za stosowne, możesz zadzwonić:

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

Pozwala to naciskać viewcontroller klienta UIKit na stos nawigacyjnym. Jeśli chcesz wrócić do szablonów TVML, po prostu wyłącz kontrolkę viewController ze stosu nawigacji.

Jeśli to, co chcesz wiedzieć, jak komunikować się między JavaScript i Swift, tutaj jest to metoda, która tworzy javascript funkcję o nazwie 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 
    }) 
} 

Po wywołaniu createPushMyView() w Swift możesz zadzwonić pod numer pushMyView() w swoim kodzie javascript, a on popchnie kontroler widoku na stos.

+2

Fantastycznie! To odpowiadało na więcej pytań niż pytanie, na które się zwrócono, takie jak interakcje pomiędzy JS i Swift w tym kontekście. Wygląda na to, że wszystkie funkcje javascript (takie jak ten przykład) powinny zostać wstrzyknięte przed wywołaniem pliku application.js. Targi? –

+0

@FrankC. Cieszę się, że ci pomogło! Tak, ponieważ ta metoda zasadniczo wstrzykuje javascript do kontekstu, musisz zadzwonić do createPushMyView() kiedyś zanim zadzwonisz pushMyView(). Jeśli spróbujesz wywołać funkcję pushMyView() w JS bez wywoływania funkcji createPushMyView() w Swift, będziesz wywoływał niezdefiniowany obiekt i nie zadziała. – shirefriendship

+0

@shirefriendship To jest świetna odpowiedź, wielkie dzięki. Chcę tylko podkreślić, że ta funkcja wypychania jest tylko przykładem tego, co wyjaśniłeś, że takie funkcje tej natury powinny być już wcześniej utworzone na stronie zaplecza JS. Jak wynika z innych przykładów i samouczków. – Placeable

-1

Tak. Zobacz TVMLKit Framework, którego dokumentacja zacząć:

Ramy TVMLKit pozwala na włączenie plików JavaScript i TVML w swoim binarne aplikacji do tworzenia aplikacji klient-serwer.

Od szybkiego skim tych docs, wygląda na to, korzystania z różnych TVWhateverFactory klas do tworzenia widoków UIKit lub widoku kontrolerów z TVML, po którym można wstawić je do aplikacji UIKit.

5

Jak wspomniano w zaakceptowanej odpowiedzi, możesz wywołać prawie dowolną funkcję Swift z poziomu kontekstu JavaScript. Zauważ, że jak sama nazwa wskazuje, setObject:forKeyedSubscript: będzie akceptować obiekty (jeśli są zgodne z protokołem dziedziczącym z JSExport) oprócz bloków, umożliwiając dostęp do metod i właściwości tego obiektu.Oto przykład

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] 
    } 
} 

Następnie w kontrolerze aplikacji:

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

Następnie można to zrobić w swoim JS: bridge.setValue(['foo', 'bar'], "baz")

Nie tylko to, ale można zastąpić widoki dla istniejących elementów, lub zdefiniuj niestandardowe elementy, które będą używane w znacznikach, i twórz kopie z natywnymi widokami:

// 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") 

Szybkie przykładem niestandardowego elementu:

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)?) { 
     //... 
    } 
} 

A oto jak założyć fabrykę niestandardowego interfejsu:

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 
    } 
} 

Niestety, widok/viewControllerForElement: nie będzie wzywał do wszystkich elementów. Niektóre z istniejących elementów (np. Widoki kolekcji) będą obsługiwać renderowanie ich elementów potomnych, bez angażowania fabryki interfejsu, co oznacza, że ​​będziesz musiał zastąpić element wyższego poziomu, lub może użyć kategorii/swizzling lub UIAppearance, aby uzyskać efekt, który chcesz.

Wreszcie, jak właśnie sugerowałem, możesz użyć UIAppearance, aby zmienić wygląd niektórych wbudowanych widoków. Oto najprostszy sposób na zmianę wyglądu TVML aplikacji zakładki paska, na przykład:

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

Jeśli masz już natywną aplikację UIKit dla tvOS, ale chciałby przedłużyć go za pomocą TVMLKit dla niektórych jej części , Możesz.

Użyj aplikacji TVMLKit jako aplikacji podrzędnej w swojej natywnej aplikacji TVOS. Poniższa aplikacja pokazuje, jak to zrobić, zachowując TVApplicationController i przedstawiając navigationController z TVApplicationController. Numer TVApplicationControllerContext jest używany do przesyłania danych do aplikacji JavaScript, ponieważ adres URL jest przesyłany tutaj:

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) 
    }