Node.jsで行うAppleTV開発

この記事は Node.js Advent Calendar 2015 4日目の記事です。

今回はAppleTV(tvOS)開発の紹介をしたいと思います。

![apple_tv_.png](https://qiita-image-store.s3.amazonaws.com/0/25794/56f02f4e-5782-0037-8f48-478f1af0625c.png)

tvOSアプリはJavaScriptで開発できる
-----

2015年10月、Appleが**新しいAppleTV**を発売しました。
この新しいAppleTVから、ようやく一般の開発者にも自由にtvOSアプリ開発できる門戸が開かれましたが、もう一つの大きな特徴としてWEBライクの開発が可能になった点が挙げられます。

ネイティブアプリケーションとして開発することも可能ですが、
今回Appleは、[TVML(Apple’s Television Markup Language)](https://developer.apple.com/library/tvos/documentation/LanguagesUtilities/Conceptual/ATV_Template_Guide/index.html)という専用**XML**言語と、[TVJS](https://developer.apple.com/library/tvos/documentation/TVMLJS/Reference/TVJSFrameworkReference/index.html)という独自クラスを持つ**JavaScript**の開発環境を用意しており、**クライアント+サーバーアプリの開発を推奨**しています。

つまり、JSとHTMLで構成されるWEBお馴染みのフローで開発が行え、Node.jsのもつ**npmの豊富なパッケージを使った開発が可能となったわけです**!

以下では、**JSは知っているけどtvOSとはなんだ、という方向けに開発方法を紹介**します。ページの遷移などでWEBとは扱い方が少々異なりますので、まずはその特徴を簡単に見てみましょう。

tvOSアプリケーションの仕組み
-----
アプリケーションがどのように動作するか、構成を見てみましょう。
動作は単純で、通常のクライアント・サーバーアプリとほぼ変わりません。

![tvos-structure.png](https://qiita-image-store.s3.amazonaws.com/0/25794/75351cf8-d15d-64c6-016f-7e849f5610e3.png)

1. **起点となるスクリプトを取得する**
まず、ネイティブアプリ側から、メインとなるTVJSをサーバーから取得することから始まります。クライアント+サーバーアプリケーションとして作る場合は、**ネイティブ側からの必要最低限の処理はこの処理のみ**になります。

2. **スクリプトをを読み込み、画面を表示する**
読み込んだメインのスクリプト(TVJS)から、次はTVMLを読み込みます。読み込んだTVMLをJS側から画面にpushすることで、画面が切り替わります。

3. **入力イベントを画面に反映させる**
ユーザーからはタップ・スワイプといった入力イベントが行われるので、イベントハンドリングを行い、アプリケーションの動作を作っていきます。
これらの動作は、TVJSで記述します。ページ遷移、ビデオの再生といったアプリケーションの基本動作は、TVJSが全て制御します。
通信処理もJSで行えるので、データや画像のリソースはアプリケーション内においておく必要はありません。


TVJSの特徴
----
独自拡張されたTVJSではどのような事ができるのでしょうか。

### tvOS用クラス
TVJSは基本的なJSの機能に加え、メディアを扱うことに特化した操作系のクラスが用意されています。例えば以下の簡単なコードで、動画をリッチなプレーヤーで再生することが可能です。

```js
// 基本的な再生手順
// 1. playerを生成
var player = new Player();

// 2. プレイリストを生成
var playlist = new Playlist();

// 3. 再生する動画を生成
var mediaItem = new MediaItem('video', videoUrl);

// 4. プレイリストに動画をセット
player.playlist = playlist;
player.playlist.push(mediaItem);

// 5. 画面に反映
player.present();
```
### ページ遷移の概念
大きくWEBと異なるのは、ページ遷移の概念です。遷移はWebページのようにURLで切り替わるのではなく、iOSアプリのような考え方で遷移します。
基本的には画面は親として持っているnavigationDocumentに対して、Stackとして次の画面がつまれていきます。
navigationDocumentを親として、ページを切り替えていくには、以下のようになります。

```js
// ページを遷移する
navigationDocument.pushDocument(document);

// ページを置き換える
navigationDocument.replaceDocument(document);

// Modalを表示する/閉じる
NavigationDocument.presentModal(modal);
NavigationDocument.dismissModal();
```
navigationBarの他には、menuBarを親としてページを切り替えることも可能です。

このように、いくつかの違いを除けば、基本的なJSの機能は利用できるので、underscore.jsやReactなどの**既存のライブラリがそのまま活用できます**。


TVMLの特徴
---------

画面の表示部分は、独自のマークアップ言語であるTVMLで記述できます。
TVMLには多数のテンプレートがあらかじめ用意されており、シチュエーションに応じてテンプレートを選択します。

![tvml-sample.png](https://qiita-image-store.s3.amazonaws.com/0/25794/44167abf-3cdb-6888-0f49-639f67b9d583.png)

例えば、楽曲一覧を表示したいと言った場合であれば、一番左の`CompilationTemplate`が使えるでしょうか。
テンプレートに沿って記述することで、[ヒューマンインターフェースガイドライン](https://developer.apple.com/tvos/human-interface-guidelines/)に準拠した見やすいページを簡単に構築することが可能です。

逆に言うと、テンプレートによる制限が多いともいえます。テンプレートは子要素に持つことが出来る種類が制限されているので、それに従わない要素は表示されません。

多くのテンプレートがあるので、[公式ドキュメント](https://developer.apple.com/library/tvos/documentation/LanguagesUtilities/Conceptual/ATV_Template_Guide/index.html)を参考にしましょう。
また、これらのコードはXMLなので、**Jadeなどのテンプレートエンジンが利用できます**。


サンプルの処理を追ってみよう
--------
さて、いくつか特徴が分かったので、公式にある基本的な処理の流れのサンプルを見てみましょう。(少し変更しています)

```js:application.js
// 1. 起動時の処理
App.onLaunch = function(options) {
    var templateURL = BASE_URL + '/index.tvml';
    getDocument(templateURL);
}

// 2. テンプレートを取得
function getDocument(url) {
    var templateXHR = new XMLHttpRequest();
    templateXHR.responseType = "document";
   
    // 3. データを取得後、navigationからページを表示
    templateXHR.addEventListener("load", function() {pushDocument(templateXHR.responseXML);}, false);
    templateXHR.open("GET", url, true);
    templateXHR.send();
    return templateXHR;
}

function pushDocument(document) {
    navigationDocument.pushDocument(document);
}
```
```xml:index.tvml
<?xml version="1.0" encoding="utf-8" ?>
<document>
    <head></head>
    <stackTemplate class="bg">
        <banner>
            <title>Welcom to AppleTV</title>
        </banner>
        <collectionList>
            <grid>
                <header></header>
                <section>
                    <lockup href="/next-page">
                        <img src="...">
                        <title>タイトル</title>
                    </lockup>
                </section>
            </grid>
        </collectionList>
    </stackTemplate>
</document>
```
先ほどの構成で説明したように、まずネイティブ側から、メインとなるスクリプト(application.js)をサーバーから取得して処理します。
次に、application.jsでは、画面を構成するindex.tvmlを取得して、その画面をnavigationDocumentからpushして表示しています。
処理としてはこれだけです。簡単ですね！

これをふまえてより複雑な、[TVMLCatalog](https://developer.apple.com/library/prerelease/tvos/samplecode/TVMLCatalog/Introduction/Intro.html)のサンプルの処理を追ってみると非常に参考になります。
このサンプルの構成では、テンプレートだけではなくTVJSもSeverから取得し、`eval`して実行していることがわかります。また、テンプレート取得後にイベントをバインドして、ユーザーがアイテムを選択した場合に、処理を追加しています。


モジュール管理に置き換える
----

上記やTVMLCatalogのサンプル構成は、アプリケーションが大規模になった場合に問題が起きます。名前空間によるモジュール管理、取得したテンプレートには同一のイベントバインド処理が走る、evalを多用している部分も問題です。
こうした構成をやめ、ルーターとBrowserifyを用いたモジュール管理に変更してみます。

```js:routes/index.js
// ルーターにページごとの処理内容を追加
var router = require('./router');

router.set('/index', function() {
    var document = documenter.make('index.jade');
    
    // ページを表示
    presenter.push(document);
});
```

```js:application.js
// 起動時にルーターのindexを呼び出す
var router = require('./router');

App.onLaunch = function(options) {
    router.redirect('index');
};
```

```js:presenter.js
exports.push = function(document) {
    // ページを表示する際に、イベントを追加
    // itemが選択されたら、hrefで指定されたルーターを呼び出す
    doc.addEventListener('select', function(event) {
        var href = element.getAttribute('href');
        if (href) {
            return router.redirect(href);
        }
    });

    navigationDocument.pushDocument(doc);
};
```

```jade:index.jade
doctype xml

mixin grid-container
    grid
        header
        section
            lockup(href='/next-page')
                img(src='...')
                title タイトル

document
    head
    stackTemplate.bg
        banner
            title Welcom to AppleTV
        collectionList
            +grid-container
```

ルーティングやモジュール管理を利用することで、たくさんのページが発生しても処理を分離できるベースができました。また階層の多くなるTVMLもJadeで綺麗に書けました。

tvOSの開発では上記のように構成を整えたり、BabelやTypeScriptなどを用いたES6時代の環境を整備して開発を行うと良いでしょう。他にも、npmの資産を利用することで、より柔軟にtvOSの開発をすすめることが出来ると思います。

（後日サンプルをアップロードします）

注意点
-----
### 利用できないライブラリ
ページ遷移の概念が異なっているため、ブラウザ利用を前提としたライブラリは利用できません。

また、windowオブジェクトを持たないなどの違いから、利用できなくなっているライブラリが多く存在するため注意が必要です。例えば、TVJSは`XMLHTTPRequest`を利用できますが、グローバルにwindowオブジェクトがあることを前提としているライブラリは`window.XMLHTTPRequest`として読み込む実装をしており、その違いでエラーが発生します。

そういった場合はWebpackなどを利用して、以下のようにグローバルにwindowオブジェクトを持たせてロードすることで読み込むことが可能になる場合があります。

```js
const superagent = require('imports?window=>{XMLHttpRequest:XMLHttpRequest}!superagent')
```

フレームワークも出始めている
------
TVML用のReact-Domや、BackboneをTVML用に置き換えたサンプルも出始めているようです。
今後の発展に期待です。
https://github.com/ramitos/react-tvml
https://github.com/frankvanrest/TVJS-TVML-Backbone


おわりに
-------
いかがでしたでしょうか。Appleが開発環境に新たにJSを用いたことは、アプリの世界にも着実にJavaScriptの世界が広がりつつあることを実感する出来事でした。

tvOSアプリの開発は、ネイティブアプリの経験の無い開発者の方でも参戦しやすい環境が整っているプラットフォームと思います。むしろ、Web開発の知識が存分に生かせる分野となっているのではないでしょうか？JSも書けてSwiftも書ける！tvOSの開発に少しでも興味を持っていただけたら幸いです。


参考
---
App Programming Guide for tvOS: The New Apple TV
https://developer.apple.com/library/prerelease/tvos/documentation/General/Conceptual/AppleTV_PG/index.html#//apple_ref/doc/uid/TP40015241-CH12-SW1

TVJS Framework Reference
https://developer.apple.com/library/tvos/documentation/TVMLJS/Reference/TVJSFrameworkReference/index.html

Apple TV Markup Language Reference: About TVML
https://developer.apple.com/library/tvos/documentation/LanguagesUtilities/Conceptual/ATV_Template_Guide/index.html

Apple TV Human Interface Guidelines - Apple Developer
https://developer.apple.com/tvos/human-interface-guidelines/

tvOSのデバッグでTVJSのconsole.logを見る方法 - Qiita
http://qiita.com/jumbOS5/items/2d557c60c69924e76ee9