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

# TVML と TVMLKit JS

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

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

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

- 公式っぽいUIコンポーネントが本当に簡単に作れる
- TVMLやJSなどのサーバサイドモジュールは、申請なしで改修できる
- ちょっと凝ったUIにしようとしたらかなり大変

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

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

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

# swiftの実装を呼び出す

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

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

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

## AppDelegateで登録する

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

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

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

```swift: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 を利用して、登録した処理を呼び出します。

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

```xml:HelloWorld.xml
<document>
    <alertTemplate>
        <title>Swiftを呼ぶよ</title>
        <button onselect="logWithSwift()">
            <text>logWithSwift()</text>
        </button>
    </alertTemplate>
</document>
```

### クラス単位で登録する

実際には、クラスに処理をまとめて、クラスごと登録する方が実用的です。
まずは `JSExport` を継承したプロトコルを定義し、そこにJSから呼び出したい処理のI/Fを定義します。

```swift:AuthManagerProtocol.swift
import TVMLKit

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

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

```swift: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`で登録します。

```swift:AppDelegate.swift
// TVApplicationControllerDelegateのメソッドを実装する
func appController(_ appController: TVApplicationController, evaluateAppJavaScriptIn jsContext: JSContext) {
    // ...

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

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

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

```xml: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