Edited at
tvOSDay 10

TVML, TVMLKit JS と swift を連携しよう

More than 1 year has passed since last update.


TVML と TVMLKit JS

みなさま、TVML, TVMLKit JS を使ってアプリ作ってますか?

tvOS向けにアプリを開発しているデベロッパの方は、キックオフ時に一度、TVMLを採用するかどうかを悩むタイミングがあると思います(ありますよね...?)

TVMLを採用した時のメリット/デメリットはいろんなところで語られていると思いますが、大きくはこんなところかと思います。


  • 公式っぽいUIコンポーネントが本当に簡単に作れる

  • TVMLやJSなどのサーバサイドモジュールは、申請なしで改修できる

  • ちょっと凝ったUIにしようとしたらかなり大変

テンプレートやUIコンポーネントは非常に強力だったりするのですが、自由なカスタムができないので、複雑なアプリには向かなかったりします。

tvOS11.0からは、TVMLKit JS に Data Binding 用のAPIが追加されたりしているのですが、癖が強くてとっつきにくかったりします。

とはいえ、第5世代のAppleTVもリリースされましたし、世の中的には動画配信サービスも有料・無料を問わず色々と流行っていますし、ちょっとTVMLに触ってみるのもいいかもしれないですね。


swiftの実装を呼び出す

TVMLで実装する場合、ロジックは基本的に TVMLKit JS で書きます。

TVMLKit JS からでもUserDefaultsやデバイス情報へのアクセスなどの簡単な処理はできるのですが、ネイティブ向けに用意されたAPIやフレームワークには、直接アクセスはできません。

つまり、課金処理やゲームセンター、キーチェーン領域へのアクセスを行うのであれば、swiftで実装を行い、TVMLやTVMLKit JS からそれらの処理を呼び出すことになります。


AppDelegateで登録する

swiftをJSから呼び出せるようにするには、あらかじめAppDelegateで登録しておく必要があります。処理の登録には、クロージャ単位で処理を登録する方法と、クラス単位で登録する方法があります。


クロージャ単位で登録する

簡単な処理であれば、直接クロージャを登録してしまうのもありかと思います。


AppDelegate.swift

// TVApplicationControllerDelegateのメソッドを実装する

func appController(_ appController: TVApplicationController, evaluateAppJavaScriptIn jsContext: JSContext) {
// 文字列を受け取って出力するだけのクロージャ.
let closure: @convention(block) (String) -> Void = {
print($0)
}
// "log"という名前でクロージャを登録する.
jsContext.setObject(closure, forKeyedSubscript: "log" as NSString)
}

TVMLやTVMLKit JS を利用して、登録した処理を呼び出します。


application.js

// 登録したクロージャを呼ぶためのfunction.

function logWithSwift() {
// 登録したクロージャを呼び出す.
log("jsからswiftを呼ぶよ")
}


HelloWorld.xml

<document>

<alertTemplate>
<title>Swiftを呼ぶよ</title>
<button onselect="logWithSwift()">
<text>logWithSwift()</text>
</button>
</alertTemplate>
</document>


クラス単位で登録する

実際には、クラスに処理をまとめて、クラスごと登録する方が実用的です。

まずは JSExport を継承したプロトコルを定義し、そこにJSから呼び出したい処理のI/Fを定義します。


AuthManagerProtocol.swift

import TVMLKit

@objc protocol AuthManagerProtocol: JSExport {
func setUserName(_ userName: String)
func userName() -> String
}


作成したプロトコルを実装したクラスを作成し、実処理を定義します。


AuthManager.swift

import UIKit

class AuthManager: NSObject, AuthManagerProtocol {
// ユーザ名の保存処理.
func setUserName(_ userName: String) {
let defaults = UserDefaults.standard
defaults.set(userName, forKey: "USER_NAME")
defaults.synchronize()
}
// ユーザ名の取得処理.
func userName() -> String {
let defaults = UserDefaults.standard
return defaults.string(forKey: "USER_NAME") ?? "(no user)"
}
}


作成したクラスは、先ほどと同様にAppDelegateで登録します。


AppDelegate.swift

// TVApplicationControllerDelegateのメソッドを実装する

func appController(_ appController: TVApplicationController, evaluateAppJavaScriptIn jsContext: JSContext) {
// ...

// "authManager"という名前で、クラス単位で登録する.
jsContext.setObject(AuthManager(), forKeyedSubscript: "authManager" as NSString)
}


登録した処理を TVML や TVMLKit JS から呼び出せるようにします。


application.js

// ユーザ名の保存処理を呼び出す.

function setUserNameWithSwift(userName) {
authManager.setUserName(userName);
}
// ユーザ名の取得処理を呼び出す.
function userNameWithSwift() {
const userName = authManager.userName();
log(`userName = ${userName}`);
}


HelloWorld.xml

<document>

<alertTemplate>

...省略

<button onselect="setUserNameWithSwift('Susumu Hoshikawa')">
<text>ユーザ名の保存処理</text>
</button>
<button onselect="userNameWithSwift()">
<text>ユーザ名の取得処理</text>
</button>
</alertTemplate>
</document>



まとめ

TVMLを採用した場合でも、多くの場合はswift層での実装も必要になると思います。

今回はJSからswiftを呼び出していますが、実際にはswiftからのコールバックをJSに渡す必要も出てくると思います。

このようにJSとswiftの連携は必須処理になると思いますので、処理の分担をしっかり設計した上で実装を行いましょう!

TVMLを採用する案件が増えて、世の中にもっと情報が出回るといいなぁ...

今回作成したプロジェクトはこちらです。

https://github.com/hoshi005/TVML-sample