LoginSignup
31
32

More than 5 years have passed since last update.

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

Posted at

オリジナルの記事はブログにあります。

全画面_2015_10_04_20_01.c93b265dd0e84da4a00d8b106aac050d.png

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

コードとしては、Appleが用意したTVMLを使用したUI Catalogを大いに参考にします。こちらはclientというディレクトを掘って、jsとtemplateとなるxml.jsを静的においていますが、今回このxml.jsを動的に生成出来ないかということを実験してみました。

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

こんな感じになります。

2015-10-04 20_08_08.25c53cd43dba4472bba10f4bc087d0f5.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 Catalogclient/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を使ってみましょう。写真含んだ要素を一覧性をもたせて積み重ねていくことができます。

スクリーンショット 2015-10-04 20.02.54.f47c4805419b4ff1b84e25ef860acaef.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がある要素でselecteventが発火すると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.jsApp.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を使ってみましょう。大きく画像を表示できるテンプレートです。
やることはさっきと同じです。oneupTemplateはリモコンの上下キーでタイトルが表示されます。

スクリーンショット 2015-10-04 20.03.16.6cdb20c41fa6463aa3ab151d5eb0b173.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はこういうったアプリがテレビのチャンネルのように並ぶことを期待しているのでしょう。そして、そういったアプリを作るのはテレビの前のあなたたちです!!(オンエアバトル風)

今回のコードはこちら

31
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
32