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で登録しておく必要があります。処理の登録には、クロージャ単位で処理を登録する方法と、クラス単位で登録する方法があります。
クロージャ単位で登録する
簡単な処理であれば、直接クロージャを登録してしまうのもありかと思います。
// 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 を利用して、登録した処理を呼び出します。
// 登録したクロージャを呼ぶためのfunction.
function logWithSwift() {
// 登録したクロージャを呼び出す.
log("jsからswiftを呼ぶよ")
}
<document>
<alertTemplate>
<title>Swiftを呼ぶよ</title>
<button onselect="logWithSwift()">
<text>logWithSwift()</text>
</button>
</alertTemplate>
</document>
クラス単位で登録する
実際には、クラスに処理をまとめて、クラスごと登録する方が実用的です。
まずは JSExport
を継承したプロトコルを定義し、そこにJSから呼び出したい処理のI/Fを定義します。
import TVMLKit
@objc protocol AuthManagerProtocol: JSExport {
func setUserName(_ userName: String)
func userName() -> String
}
作成したプロトコルを実装したクラスを作成し、実処理を定義します。
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
で登録します。
// TVApplicationControllerDelegateのメソッドを実装する
func appController(_ appController: TVApplicationController, evaluateAppJavaScriptIn jsContext: JSContext) {
// ...
// "authManager"という名前で、クラス単位で登録する.
jsContext.setObject(AuthManager(), forKeyedSubscript: "authManager" as NSString)
}
登録した処理を TVML や TVMLKit JS から呼び出せるようにします。
// ユーザ名の保存処理を呼び出す.
function setUserNameWithSwift(userName) {
authManager.setUserName(userName);
}
// ユーザ名の取得処理を呼び出す.
function userNameWithSwift() {
const userName = authManager.userName();
log(`userName = ${userName}`);
}
<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