RailsでApple TV向けアプリをつくる

> オリジナルの記事は[ブログ](http://www.imaimiami.com/tvml-rails-app/)にあります。

![全画面_2015_10_04_20_01.c93b265dd0e84da4a00d8b106aac050d.png](https://qiita-image-store.s3.amazonaws.com/0/29793/4856cd88-4241-01ed-ddaa-915b4992422b.png)

Apple TV向けのアプリは、iOSネイティブ開発で使われるView Controllerなどを使わず、jsとTVMLという謎マークアップで作成することができます。
Apple大先生もバイナリをアップロードしなくていいから迅速にアップデートできるよ的なことを[言ってるので](https://forums.developer.apple.com/thread/17707)、これはWeb技術者の出番なのではないでしょうか。
tvOSでWeb Viewが利用不可らしいことから考えて、おそらくtvでweb的なことをするならTVML使ってUIを統一しろよ！って事なのだと私は理解しました。

コードとしては、Appleが用意したTVMLを使用した[UI Catalog](https://developer.apple.com/library/prerelease/tvos/samplecode/TVMLCatalog/Introduction/Intro.html#//apple_ref/doc/uid/TP40016505-Intro-DontLinkElementID_2)を大いに参考にします。こちらは`client`というディレクトを掘って、jsとtemplateとなる`xml.js`を静的においていますが、今回この`xml.js`を動的に生成出来ないかということを実験してみました。

作るアプリはDribbbleの最新人気投稿を表示するアプリケーションです。前回のReact Nativeのネタと全く同じです。ごめんなさい。Dribbbleは素晴らしいですね。私もいつかDribbblerになりたいものです。

こんな感じになります。

![2015-10-04 20_08_08.25c53cd43dba4472bba10f4bc087d0f5.gif](https://qiita-image-store.s3.amazonaws.com/0/29793/e3426f76-299a-fe09-6786-e1fe68ee3f11.gif)


### 開発準備
1. Xcode7.1betaをインストール、アプリケーションの雛形を作成。
まだbetaです。tvOSのSingleViewApplicationを作成します。
2. AppDelegate.swiftにjs/TVMLのアプリをサーバーから配信するぜって、ごにょごにょ記載。

TVMLKitをimportして、UIApplicationDelegate,TVApplicationControllerDelegateを追加します。

`appControllerContext.javaScriptApplicationURL`にサーバサイドのjsファイルの場所を指定します。詳細は下に続きますが、このjsファイルにはアプリケーション起動時の処理(`App.onLaunch`)が書かれます。

また、`appControllerContext.launchOptions["BASEURL"] `のように`launchOptions`に値を渡しておくと、jsファイルの方で`options`オブジェクトにぶら下がった状態で受け取れます。

	import UIKit
	import TVMLKit
	
	@UIApplicationMain
	class AppDelegate: UIResponder, UIApplicationDelegate, TVApplicationControllerDelegate {
	
	    // MARK: Properties
	    
	    var window: UIWindow?
	    
	    var appController: TVApplicationController?
	    
	    static let TVBaseURL = "http://localhost:3000/"
	    
	    static let TVBootURL = "\(AppDelegate.TVBaseURL)js/application.js"
	    
	    // MARK: UIApplication Overrides
	    
	    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
	        window = UIWindow(frame: UIScreen.mainScreen().bounds)
	        
	        let appControllerContext = TVApplicationControllerContext()
	        
	        if let javaScriptURL = NSURL(string: AppDelegate.TVBootURL) {
	            appControllerContext.javaScriptApplicationURL = javaScriptURL
	        }
	        
	        appControllerContext.launchOptions["BASEURL"] = AppDelegate.TVBaseURL
	        
	        if let launchOptions = launchOptions as? [String: AnyObject] {
	            for (kind, value) in launchOptions {
	                appControllerContext.launchOptions[kind] = value
	            }
	        }
	        
	        appController = TVApplicationController(context: appControllerContext, window: window, delegate: self)
	        
	        return true
	    }
	...
	
	}

そして、`rails new client`します。とりあえずお試しなので、同じリポジトリでいいじゃんということです。既にRailsを使ってサービスを運営しているならtvosなどnamespaceを切ってお手軽に配信出来るかもですね。

### Rails下準備
通常のwebやapiサービスを作るわけじゃないので、ちょっと下準備します。  
基本的に開き直った感じのことしかしないです。

#### `.xml.js`を返すようにする。

今回はhtmlでもなく、jsonでもなくjsを返します。
この辺はrailsサイドでもう少しスマートに出来ると思いますが、今回はファイル名指定で突き進みます。xmlを返すようにしてapplication.js側での対応も可能なら綺麗かもしれません。

	def index
	 render "index.xml.js"
	end


#### `/app/controllers/application_controller.rb`でCSRF 対策を無効にする。

セキュリティの神に一礼し、おもむろにコメントアウトしましょう。

	class ApplicationController < ActionController::Base
	  # Prevent CSRF attacks by raising an exception.
	  # For APIs, you may want to use :null_session instead.
	  # protect_from_forgery with: :exception
	end

#### `public/js`以下にtwosアプリから最初に呼び出すjsファイルを置く。
[UI Catalog](https://developer.apple.com/library/prerelease/tvos/samplecode/TVMLCatalog/Introduction/Intro.html#//apple_ref/doc/uid/TP40016505-Intro-DontLinkElementID_2)の`client/js`以下のファイルを参考に`application.js`を設置します。自分は少しいらない機能を消したりしました。
各ファイルの役割は以下の様な感じです。
##### `ResourceLoader.js`
リモートのテンプレートファイルを取得して、読み込みます。jsを評価してxmlで書かれた文字列が代入された変数を取得してます。
##### `Presenter.js`
画面表示周りを担ってます。tvosが提供しているapi、画面表示(`navigationDocument.pushDocument`)と、モダル表示(`navigationDocument.presentModal`)を扱いやすくしたメソッドが用意されており、画面でアクションした時に画面遷移するためのevent binding(`doc.addEventListener("select" function…)`)も担当します。

##### `application.js`
tvosのアプリから最初に読み込むファイルです。起動時に実行される関数`App.onLaunch`を記載します。onLaunchの中で`ResouceLoader`を使って最初に表示するページを読み込み、`Presenter`で表示してます。

#### `rails generate scaffold shots`する
今回はdribbleの人気shotを表示するアプリを作るので、shots controllerなど用意します。

#### 一覧画面を作る
`shots#index`です。TVMLは基本的にAppleから提供されているコンポーネントを使って組み立てていきます。indexページの大枠のテンプレートには[`stackTemplate`](https://developer.apple.com/library/prerelease/tvos/documentation/LanguagesUtilities/Conceptual/ATV_Template_Guide/StackTemplate.html#//apple_ref/doc/uid/TP40015064-CH21-SW4)を使ってみましょう。写真含んだ要素を一覧性をもたせて積み重ねていくことができます。

![スクリーンショット 2015-10-04 20.02.54.f47c4805419b4ff1b84e25ef860acaef.png](https://qiita-image-store.s3.amazonaws.com/0/29793/ac325fdd-2e4a-2733-c69b-cca834d3d455.png)

とりあえず、コントローラーで最新の人気投稿を5ページ分とってきます。
`app/controllers/shots_controller.rb#index`

	def index
	    @shot_shelfs = []
	    (1..5).each do |page|
	      res = open("http://api.dribbble.com/shots/popular?page=#{page}")
	      code, message = res.status # res.status => ["200", "OK"]
	
	      if code == '200'
	        result = ActiveSupport::JSON.decode res.read
	        @shot_shelfs << result["shots"]
	      else
	        puts "#{code} #{message}"
	      end
	    end
	
	    render "index.xml.js"
	  end

テンプレートはerbで強引に出力してます。imgには高さと幅を指定しなければなりません。ただし、グリッドの要素全てで同じ高さと幅でなければならないというわけではなかったです。また、templateというattributeがある要素で`select`eventが発火するとtemplateの値のページに遷移します。これは`Presenter`が担う役割です。

`app/views/shots/index.xml.js.erb`

	var Template = function() { return `<?xml version="1.0" encoding="UTF-8" ?>
	  <document>
	    <stackTemplate>
	      <banner>
	        <title>Dribbble Popular Shots</title>
	      </banner>
	      <collectionList>
	        <% @shot_shelfs.each do |shots| %>
	          <shelf>
	            <section>
	                <% shots.each do |shot| %>
	                <lockup template="${this.BASEURL}shots/<%= shot["id"] %>">
	                  <img src="<%= shot["image_url"] %>" height="200" width="200"/>
	                  <title><%= shot["title"] %></title>
	                </lockup>
	              <% end %>
	            </section>
	          </shelf>
	        <% end %>
	      </collectionList>
	    </stackTemplate>
	  </document>`
	}

あとは、`application.js`の`App.onLaunch`でshots#indexを開くように指定しましょう。今更ですが、Railsのassets以下の`application.js`は使ってないです。紛らわしかったですね。
`evaluateScripts`には配列が渡せるらしいので、ライブラリを評価し終わった後に、shots#indexのテンプレートを読み込んで、表示します。
`/public/js/application.js`

	App.onLaunch = function(options) {
		var javascriptFiles = [
		 `${options.BASEURL}js/ResourceLoader.js`,
		 `${options.BASEURL}js/Presenter.js`
		];
	
		evaluateScripts(javascriptFiles, function(success) {
			if(success) {
				resourceLoader = new ResourceLoader(options.BASEURL);
				resourceLoader.loadResource(`${options.BASEURL}shots`, function(resource) {
					var doc = Presenter.makeDocument(resource);
					doc.addEventListener("select", Presenter.load.bind(Presenter));
					Presenter.defaultPresenter(doc);
				})
			} else {
				var errorDoc = createAlert("Evaluate Scripts Error", "Error attempting to evaluate external JavaScript files.");
				navigationDocument.presentModal(errorDoc);
			}
		});
	}

#### 詳細画面を作る
`shots#show`です。テンプレートは[`oneupTemplate`](https://developer.apple.com/library/prerelease/tvos/documentation/LanguagesUtilities/Conceptual/ATV_Template_Guide/OneupTemplate.html#//apple_ref/doc/uid/TP40015064-CH26-SW1)を使ってみましょう。大きく画像を表示できるテンプレートです。
やることはさっきと同じです。oneupTemplateはリモコンの上下キーでタイトルが表示されます。

![スクリーンショット 2015-10-04 20.03.16.6cdb20c41fa6463aa3ab151d5eb0b173.png](https://qiita-image-store.s3.amazonaws.com/0/29793/dbe1cfc2-ec1d-ba37-6ca9-65ee97e7fda0.png)

`/app/controllers/shots_controller.rb`

	def show
	    res = open("http://api.dribbble.com/shots/#{params[:id]}")
	    code, message = res.status # res.status => ["200", "OK"]
	
	    if code == '200'
	      result = ActiveSupport::JSON.decode res.read
	      @shot = result
	    else
	      puts "#{code} #{message}"
	    end
	
	    render "show.xml.js"
	  end

`/app/views/shots/show.xml.js.erb`

	var Template = function() { return `<?xml version="1.0" encoding="UTF-8" ?>
	  <document>
	    <oneupTemplate mode="oneup caption" allowsZooming="false">
	      <section>
	        <lockup>
	          <img src="<%= @shot["image_url"] %>" />
	          <title><%= @shot["title"] %></title>
	          <row>
	            <subtitle><%= @shot["player"]["name"] %></subtitle>
	          </row>
	        </lockup>
	      </section>
	    </oneupTemplate>
	  </document>`
	}

#### 最後に
以上で、人気投稿を一覧して、拡大してみるアプリができました。簡単でしたね。
きっとApple TVはこういうったアプリがテレビのチャンネルのように並ぶことを期待しているのでしょう。そして、そういったアプリを作るのはテレビの前のあなたたちです！！（オンエアバトル風）

今回のコードは[こちら](https://github.com/imaimiami/tvos_rails_tvml_dribbble_reader_sample)