オリジナルの記事はブログにあります。
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になりたいものです。
こんな感じになります。
開発準備
- Xcode7.1betaをインストール、アプリケーションの雛形を作成。 まだbetaです。tvOSのSingleViewApplicationを作成します。
- 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の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
を使ってみましょう。写真含んだ要素を一覧性をもたせて積み重ねていくことができます。
とりあえず、コントローラーで最新の人気投稿を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
を使ってみましょう。大きく画像を表示できるテンプレートです。
やることはさっきと同じです。oneupTemplateはリモコンの上下キーでタイトルが表示されます。
/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はこういうったアプリがテレビのチャンネルのように並ぶことを期待しているのでしょう。そして、そういったアプリを作るのはテレビの前のあなたたちです!!(オンエアバトル風)
今回のコードはこちら