Edited at

Apple TV のメモ

More than 3 years have passed since last update.


Siri Remote


Siri Remote の対応国

Siri Remote は、特定の国でのみ同梱されている。

同梱されているのは以下の国


  • オーストラリア

  • カナダ

  • フランス

  • ドイツ

  • 日本

  • スペイン

  • 英国

  • 米国


それ以外の国の場合

リモコン上のSiriボタンを押しても、上記の国以外では、Siriは起動しない。

その代わり、オンスクリーン検索アプリなるアプリが起動する模様。


個人的に気になっていたホームボタン

⑤TVマークのボタンは、ホームボタン


  • 押すとホームボタンに戻る

  • 2回押すと起動中のアプリ一覧が表示される(ランチャー状態)

  • 長押しするとスリープになる


動画の再生について

Apple TV 上の動画再生はすべて、HTTP Live StreamingFairPlay Streamingをベースとしたもの。

HLSの素材制作仕様に関しては、こちらを参照。


トップシェルフ

ホーム画面の上部5つに並べられたアプリは、ホーム画面上部の領域を利用できる。

ここからアプリ内のコンテンツに直接遷移したりすることも可能。


フォーカスとパララックス画像

あるUI要素にフォーカスが当たっている状態とは、ユーザがハイライトしたけど、まだ決定はしていないという状態。

レイヤード画像にフォーカスが当たると、Touchサーフィスにおける操作で、パララックス効果を生み出す。

UIImageViewがレイヤード画像を扱えるように拡張されたので、コードは簡単である。ただし、バンドルされたレイヤード画像と、サーバから取得したレイヤード画像では、扱い方が異なる。


新しいtvOSフレームワーク

tvOS向けに新しく追加されたフレームワークは、以下の通りである。


  • TVMLJS


    • TVMLページを読み込むためのJavaScriptのAPI群

    • クライアント - サーバ型のアプリで、情報を表示するために利用する

    • Apple TV JavaScript Framework Reference



  • TVMLKit


    • JavaScriptやTVMLをアプリに組み込むためのもの

    • TVMLKit Framework Reference



  • TVServices


    • トップシェルフエクステンションをアプリに追加するためのもの

    • TVServices Framework Reference




引き続き利用可能なフレームワーク


  • Accelerate

  • AudioToolbox

  • AudioUnit

  • AVFoundation

  • AVKit

  • CFNetwork

  • CloudKit

  • CoreBluetooth

  • CoreData

  • CoreFoundation

  • CoreGraphics

  • CoreImage

  • CoreLocation

  • CoreMedia

  • CoreSpotlight

  • CoreText

  • CoreVideo

  • Darwin

  • Foundation

  • GameController

  • GameKit

  • GameplayKit

  • GLKit

  • ImageIO

  • MachO

  • MediaAccessibility

  • MediaPlayer

  • MediaToolbox

  • Metal

  • MetalKit

  • MetalPerformanceShaders

  • MobileCoreServices

  • ModelIO

  • OpenGLES

  • SceneKit

  • Security

  • simd

  • SpriteKit

  • StoreKit

  • Swift Standard Library

  • SystemConfiguration

  • UIKit


MacやiOSデバイスとの違いなど

Apple TV にはマウスがないので、直接選択して操作するとこも、ジェスチャーやタッチすることもできない。代わりに、Siri Remoteゲームコントローラーを利用して操作する。


アプリのストレージについて

Apple TV アプリは、そのバイナリを200MB以内に収める必要がある。

デバイス上のローカルなストレージは、500KBまでしかアクセスできない(NSUserDefaultsを利用)。この範囲を超えた場合は、OSが破棄する。

以上の状況を回避するためには、以下の手段がとれる


  • iCloudを利用して保存する

  • キャッシュディレクトリに一時保存する


    • アプリ起動中は消えない

    • アプリが終了していると、削除される可能性がある



  • オンデマンドリソースを利用する



アプリ内で利用するアセットリソースはオンデマンドリソース、ユーザデータはiCloudに保存するようにする必要がある。


Apple TV を対象とするコード

tvOSのみを対象にするコードは、以下のマクロを利用する

#define TARGET_OS_TV 1


クライアント - サーバ 型アプリケーションの開発

Xcodeで開発するプロジェクトの主な役割は、メインとなるJavaScriptファイルを読み込んで実行し、TVMLファイルから生成したページを画面上に表示すること。

JavaScriptのコードは、TVMLページを読み込み、各ページをナビゲーションスタックにプッシュする。ユーザの操作に応じて、TVMLページをそのスタックからPush/Popする。ユーザがアプリを終了すると、Apple TV のホーム画面が表示される。


アプリの構築手順


  1. Xcodeを起動し、新規プロジェクトを作成する

  2. tvOSテンプレート群のうち、Single View Application を選択する

  3. ビューコントローラのファイル、アプリケーションのメインのストーリーボードを削除する

  4. info.plist を開き、メインストーリーボードのベース名を表すエントリを削除する

  5. AppDelegate.swiftファイルに以下のような記述を追記する



    • import TVMLKit を追記

    • プロトコル TVApplicationControllerDelegate を実装する

    • グローバル変数 var appController: TVApplicationController? を追加する


    • application:didFinishLaunchingWithOptions: の実装コードを以下のように変更する



func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

self.window = UIWindow(frame: UIScreen.mainScreen().bounds)

let appControllerContext = TVApplicationControllerContext()

let javascriptURL = NSURL(string: "Enter path to your JavaScript file here")

appControllerContext.javaScriptApplicationURL = javascriptURL!
if let options = launchOptions {
for (kind, value) in options {
if let kindStr = kind as? String {
appControllerContext.launchOptions[kindStr] = value
}
}
}

self.appController = TVApplicationController(context: appControllerContext, window: self.window, delegate: self)

return true
}

このコードは、JavaScriptファイルを読み込んだ後、TVMLページを読み込んで、シミュレータ、あるいはApple VT がコンピュータに接続されている場合、接続されているテレビ画面上に表示する。

以下のJavaScriptコードは、アプリをプレミアム版にアップグレードするよう促す警告ページのTVMLページを読み込む。ページの読み込み後、ナビゲーションスタックにそのページがPushされる。そしてtvOSがこれを実際に表示する。

function getDocument(url) {

var templateXHR = new XMLHttpRequest();
templateXHR.responseType = "document";
templateXHR.addEventListener("load", function() {pushDoc(templateXHR.responseXML);}, false);
templateXHR.open("GET", url, true);
templateXHR.send();
return templateXHR;
}

function pushDoc(document) {
navigationDocument.pushDocument(document);
}

App.onLaunch = function(options) {
var templateURL = 'Enter path to your server here/alertTemplate.tvml';
getDocument(templateURL);
}

App.onExit = function() {
console.log('App finished');
}

<document>

<alertTemplate>
<title>Update to premium</title>
<description>Go ad free by updating to the premium version</description>
<button>
<text>Update Now</text>
</button>
<button>
<text>Cancel</text>
</button>
</alertTemplate>
</document>


Important

JavaScriptおよびXMLファイルをWebサーバから配布する場合、サーバ側でページの内容を変更したら、クライアントアプリ側の表示に、確実にその変更を反映させる必要があります。そのためには、クライアント側のキャッシュに、ページ内容を保存しないようにしなければなりません。HTTPリクエストに応じてページを返す歳、ャッシュしてはならない旨を伝えるため、Cache-Control:no-cacheというヘッダをHTTPレスポンスに入れてください。


Apple TV Remote によるユーザインターフェース操作

iOSデバイスの場合、ユーザは直接タッチスクリーン上でインターフェイスを操作します。これに対しApple TVでは、リモコンを通じて間接的にインターフェイスを操作します。画面上のある項目にナビゲートした後、リモコンのボタンを押して洗濯する、という操作です。画面上の各項目は、そこにナビゲートすると「フォーカスされた」状態になります。フォーカスとは、リモコンその他、外付け入力デバイスからの間接的なユーザー入力に応じて画面上に現れる表示効果のことです。フォーカスベースの操作モデルでは、画面上の「ひとつだけの」ビューに対してフォーカスがあたります。ユーザは、画面上の様々なUI要素間をナビゲートする形で、フォーカスを他のビューに移動できます。その結果、画面上では、フォーカスが更新されます。フォーカスされたビューは、ユーザアクションの対象になります。例えば、あるボタンがフォーカスされた状態であれば、リモコンから選択されたイベントっが送信された時、該当ボタンのアクションを起動することになります。

UIKitフレームワークは、フォーカスベースのインターフェイスにのみ対応しています。多くの場合、特段の設定をしなくても、上記のような挙動になります(それが意味をなす限り)。プログラムからはフォーカスの更新を依頼することは可能ですが、フォーカスをセットしたり、ある向きに移動したりすることはできません。また、たとえばUIButtonオブジェクトはフォーカス可能ですが、UILabelオブジェクトはできません。独自のユーザインターフェイスコンポーネントをアプリケーションに組み込む場合、フォーカスに関する挙動も実装する必要があります。フォーカス可能なUIKitの項目は以下のようなものがあります。


  • UIButton

  • UITextField

  • UICollectionView

  • UITextView

  • UISegmentedControl

  • UISearchBar


フォーカスエンジンによるフォーカスの制御

フォーカスやその動きを制御するUIKit内のシステムを、フォーカスエンジンと言います。ユーザは、(各種の)リモコン、ゲームコントローラ、シミュレータなどを使って、フォーカスを制御できます。フォーカスエンジンは、このような入力デバイスから届くフォーカス移動イベントを監視しています。イベントを認識すると自動的に、フォーカスを更新するか否か判断し、アプリケーションに通知します。アプリケーションによって使い勝手が異なってしまわないよう、現行および将来的な入力方法全てに対して、このシステムが自動的に働きます。また、この仕組みは開発者にとっても、基本的なナビゲーション機能を独自に定義したり再発明する必要をなくし、アプリケーション独自の挙動の実装に集中することができます。

フォーカスを更新できるのはフォーカスエンジンのみです。どのビューにフォーカスを当てるか直接指定したり、特定の方向にフォーカス移動したりするAPIはありません。ユーザが移動イベントを送信した時や、システムが更新を要求した時、或いは、アプリケーションが更新を要求した時にのみ、フォーカスエンジンがフォーカスを更新します。フォーカスをプログラムからいつ、どのように更新するかに関しては、"プログラムによりフォーカスを更新する"を参照してください。

フォーカスエンジンは、フォーカスが予想外の動きをしないよう、また、どのアプリケーションでも同じ挙動になるように制御します。ユーザが混乱しないだけでなく、開発者に取っても、適切な制御コードを独自に実装する必要がない、という利点があります。


UIFocusEnvironmentプロトコル

フォーカスエンジンは、UIFocusEnvironmentプロトコルに従ってアプリケーションと通信します。このプロトコルはビュー階層の一部におけるフォーカスの挙動を定義するプロトコルです。このプロトコルを実装しているUIKitのクラスは、以下の通りです。


  • UIView

  • UIViewController

  • UIWindow

  • UIPresentationController

これらのクラスは、直接または間接的に、画面上のビューを制御するクラスです。アプリケーションでは、ビューやビューコントローラー内に定義するUIFocusEnvironmentプロトコルのメソッド群をオーバーライドすることにより、フォーカスの挙動を制御します。


ユーザの操作に応じたフォーカスの移動

リモコンやその他の入力デバイスからナビゲーションイベントが届くと、フォーカスエンジンは自動的にフォーカスの移動先を判断します。ユーザはフォーカスを上下左右に移動できるほか、ハードウェアによっては対角線方向の移動も可能です。例えば左にスワイプすると、フォーカスエンジンは、フォーカス可能なビューのうち、現在フォーカスが当たっているビューよりも左側にあるものを探します。該当するビューが見つかった場合はフォーカスをそこに移動しますが、なければ移動しません。

入力デバイスによっては、より洗練された挙動が求められますが、フォーカスエンジンはこれにも自動的に対応します。例えば、リモコンを素早くスワイプした後の慣性を考慮した動き、フォーカス速度に応じたアニメーションの速度調整、ナビゲーション音の再生、画面外にフォーカスが移動した時にスクロールビューのオフセットを調整する処理などがあります。フォーカス移動に伴うアニメーションについて詳しくは、UIFocusAnimationCoordinator Class Referenceを参照してください。


フォーカスの移動先の決定

ユーザのアクションに応答してフォーカスの移動先を決定する際、フォーカスエンジンは、アプリケーションのユーザインターフェイスに使われている内部構造を判断し、フォーカス可能なビューの可視領域をすべてハイライトします。フォーカス可能であっても、ほかのビューの奥に完全に隠れているものは無視します。一部でも見えてみるものだけが対象になります。このテクニックにより、現在フォーカスされているビューを起点として、動きの方向にあるフォーカス可能な領域を見つけます。検索する範囲は、現在フォーカスされているビューの大きさに応じて決まります。

フォーカスエンジンは移動先のビューを見つけた後、実際に移動する前に、移動が適切かどうか判断する機会をアプリケーションに与えます。そのためオペレーティングシステムは、移動元と移動先のビューを含むフォーカス環境それぞれについて、shouldUpdateFocusInContext:メソッドを呼び出します。移動元のビュー、移動先のビュー、そして最後にこの2つの親であるビュー、という順序で呼び出します。いずれか一つでも、shouldUpdateFocusInContext:の戻り値がNOであれば、移動を取り止めます。フォーカス環境について詳しくは、UIFocusEnvironment Protocol Referenceを参照してください。


初期フォーカスとプリファードフォーカスチェーン

アプリケーションの起動時、フォーカスエンジンはフォーカスがあたる最初のビューを決定します(特に指定がなければ)。通常は、フォーカス可能なビューのうち、画面の左上角に最も近いものを選択します。

一方で、アプリケーションは、どのビューにフォーカスを当てるべきか、UIFocusEnvironmentプロトコルのプリファードフォーカスビューの仕組みを使ってヒントを与えることができます。初期フォーカスを設定する際、フォーカスエンジンはまず、ルートウィンドウのプリファードフォーカスビューを問い合わせます。その戻り値は、ルートビューコントローラのpreferredFocusedViewオブジェクトとして指定されているビューです。この戻り値はUIViewオブジェクトであり、UIFocusEnvironmentを実装しているため、フォーカスエンジンは、このUIViewオブジェクトに対してプリファードフォーカスビューを問い合わせ、以下同様に継続します。このように、プリファードフォーカスビューを順次たどって得られるリストを、プリファードフォーカスチェーンと言います。フォーカスエンジンは、selfまたはnilに到達するまで、このプリファードフォーカスチェーンを辿ります。このリストの末尾に当たる、初期ウィンドウから最も遠い(深い)ビューを、フォーカス可能なビューとして選択します。


フォーカスを判断する過程の例:



  1. フォーカスエンジンはルートウィンドウに、そのpreferredFocusedViewを問い合わせます。戻り値はルートビューコントローラのpreferredFocusedViewオブジェクトです。

  2. このルートビューコントローラは、例えばタブビューコントローラの場合、戻り値であるpreferredFocusedViewオブジェクトは、選択されたタブのビューコントローラです。

  3. 選択されたビューコントローラはpreferredFocusedViewメソッドをオーバーライドしており、あるUIButtonのインスタンスを返します。


  4. UIButtonインスタンスはプリファードフォーカスビューとしてself(デフォルト値)を返します。このボタンはフォーカス可能なので、フォーカスエンジンはこれを、次にフォーカスするビューとして選択します。

フォーカスを更新すると、ここで選択されたビュー(プリファードフォーカスチェーンの末尾に当たるビュー)に、新たにフォーカスが付与されます。もうひとつ、フォーカスチェーンの例を示しましょう。現在フォーカスされているビュー「C」の上に、モーダル型のビューコントローラ「M」が現れたとします。この状態でフォーカスを更新すると、(ルートや「C」ではなく)「M」を起点とするプリファードフォーカスチェーンに基づいて、フォーカスの移動先が決まります。


フォーカスの更新

フォーカスの更新が発生するのは、ユーザーがフォーカスを移動した(たとえば、リモコン上でスワイプした)とき、アプリケーションがプログラムから更新を要求した時、システムが自動更新処理を起動した時などです。


フォーカス更新の仕組み

フォーカスの更新、あるいは新しいビューに移動する際、それがサブビューであるか、ビュー階層の他の部分に属するビューかによらず、次のイベントが発生します。


  • focusedViewプロパティを、新たにフォーカスされたビュー、またはそのプリファードフォーカスビューに更新します。

  • フォーカスエンジンは、これまでフォーカスがされていたビュー、または次に(更新後に)フォーカスがされるビューを含むそれぞれのフォーカス環境に対して、didUpdateFocusInContext:withAnimationCoordinator:を呼び出して通知します。この通知に応じて、組み込まれたアニメーションコーディネータを使い、フォーカスの移動に伴うアニメーションの表示スケジュールを設定できます。詳細については、UIFocusAnimationCoordinatorを参照してください。

  • すべての関係するフォーカス環境に対して通知をした後、各アニメーションの再生が同時に開始されます。

  • 次に(更新後に)フォーカスされるビューがスクロールビュー内にあって、画面から外れている場合、このビューが画面内に入るよう中身がスクロールします。


システムが起動するフォーカス更新

UIKitはさまざまな状況で、必要に応じてフォーカスを自動更新します。たとえば次のような状況です。


  • フォーカスされているビューを、ビュー階層から削除したとき。


  • UITableViewUICollectionViewがデータを再読み込みした時。

  • 新しいビューコントローラが表示され、現在フォーカスされているby−がその奥に隠れてしまった時。

  • ユーザーがリモコンのMenuボタンを押して前画面に戻った時。


プログラムによりフォーカスを更新する

多くの場合、フォーカスは必要に応じて自動更新されますが、アプリケーション側から、更新処理を起動するコードを実装する必要が生じることもあります。すべてのフォーカス環境はsetNeedsFocusUpdateを呼び出すことにより。フォーカスを更新するよう要求できます。これにより該当環境のpreferredFocusedViewに、フォーカスをリセットすることになります。このようなアプリケーション側からのプログラムによるフォーカス更新が必要になるのは、たとえば次のような場合です。


  • アプリケーションの内容が変わり、ユーザーが期待するような挙動にするため、フォーカスの更新を要する時。たとえば音楽再生アプリケーションの場合、再生中の曲が常にフォーカスされている必要があります。ある曲の再生が終わったら、プレイリストの次の曲にフォーカスを移動しなければなりません。

  • アクションに応じてフォーカスが移動する、という挙動をユーザが期待する場合。たとえばスプリットビューで、左側にメニュー、右側に一連の情報が表示されているとします。メニュー間を移動すると、右側の表示内容が変わります。その後、あるメニューを選択すると、対応して右側に現れる表示内容のうち先頭の項目に、自動的にフォーカスが移動するとユーザーは期待するでしょう。

  • 独自に実装したコントロール部品が、フォーカスされている間に内部状態が変化し、フォーカスの移動を要する場合。たとえば、リモコンをクリックしてピッカーを選択すると、フォーカスが移動し、そのピッカー内の選択肢からいずれかを選択できるようになります。リモコンの「Menu」ボタンを押すと、元のコントロール部品にフォーカスが戻ります。この場合、コントロール部品の選択に応じて、フォーカスを更新することにより、preferredFocusedViewで示されるサブビューにフォーカスを移動するよう要求する必要があります。


アプリケーションでフォーカスを管理する

組み込みのUIKitコントロール部品を使っている限り、独自にフォーカス処理を実装する必要はありません。アプリケーションを起動すると、画面上にあるフォーカス可能ないずれかのコントロール部品にフォーカスがあたり、その後も一貫してフォーカスエンジンがフォーカスを管理します。

しかしアプリケーションにおいて、フォーカスに関する独自の挙動を実装したいケースもあるでしょう。例えば、フォーカスが変化した時アプリケーションの状態を更新する、新しい種類のインタラクティブなUI要素を実装する、フォーカスの移動に伴って独自のアニメーションを再生する、などが考えられます。以下の節では、独自の挙動を実装する上で知っておくべき事項の概要を示します。

UIFocusEnvironmentは、ビュー階層のある枝にあたる要素に対して適用する、フォーカス関連の挙動を制御します。例えば、UIViewControllerはそのルートビュー及び子孫、UIViewは自分自身及びその子孫について、フォーカス関連の挙動を制御するようになっています。このように、ビュー階層の同じ枝にある要素に対して、複数のフォーカス環境が挙動を制御することもありえます。ビューは、別のビューの子であると同時に、ビューコントローラの子でもあるからです。フォーカス関連の挙動を制御すると言っても、何かにフォーカスをあてるということだけではありません。フォーカス環境は、ビュー階層内でどのようにフォーカスが移動するか、これにUIがどのように反応するか、を制御することも可能です。


ビューコントローラでフォーカスを処理する

UIViewControllerUIFocusEnvironmentプロトコルを実装しているので、独自のビューコントローラを組み込んでいる場合UIFocusEnvironmentのデリゲートメソッドを上書きすることにより、フォーカスに関する独自の挙動を実現できます。具体的には次のようなことが可能です。



  • preferredFocusedViewをオーバーライドし、最初にどこにフォーカスを当てるかを指定します。


  • shouldUpdateFocusInContext:をオーバーライドし、フォーカスをどこに移動できるかを定義します。


  • didUpdateFocusInContext:withAnimationCoordinator:をオーバーライドし、フォーカスの更新に対して応答し、アプリケーションの内部状態を更新します。

さらにビューコントローラは、setNeedsFocusUpdateを呼び出すことにより、preferredFocusedViewで示されるビューにフォーカスをリセットするよう、フォーカスエンジンに要求できます。ただし、setNeedsFocusUpdateを呼び出して効果があるのは、現時点でフォーカスされているビューが、該当ビューコントローラに含まれる場合に限ります。


コレクションビューやテーブルビューでフォーカスを処理する

コレクションビューやテーブルビューでは、デリゲートオブジェクトを使って独自の挙動を定義します。これはフォーカスベースのインターフェイスの実装に関しても同様です。UITableViewDelegateUICollectionViewDelegateといったプロトコルには、UIFocusEnvironmentプロトコルと同様のメソッドやプロパティが宣言されていますが、いずれもテーブルビューやコレクションビューを対象とするものです。コレクションビューやテーブルビューのフォーカスを制御する上で、知っておくと役に立つヒントをいくつか示します。



  • collectionView:canFocusItemAtIndexPath:メソッドはUICollectionViewDelegateクラス、tableView:canFocusRowAtIndexPath:メソッドはUITableViewDelegateクラスを使って、あるセルがフォーカス可能か否かを指定します。これには、独自に実装したUIViewcanBecomeFocusedメソッドをオーバーライドするのと同様の働きがあります。


  • UICollectionViewUITableViewにも、remembersLastFocusedIndexPathプロパティを使って、コレクションビューやテーブルビューからフォーカスを解除した後、再び同じビューに入った時、直近にフォーカスインデックスパスに戻すか否かを指定できます。


独自のビューでフォーカスを処理する

UIViewControllerと同様、UIViewUIFocusEnvironmentプロトコルを実装しているので、ビューコントローラでフォーカスを処理するで概説した事項は独自のビューにも当てはまります。ただし、ビューはフォーカス可能な場合もあるので、フォーカスに関する独自の挙動を実装する際には、特別な検討が必要です。


  • 独自ビューをフォーカス可能にする必要があれば、canBecomeFocusedをオーバーライドし、YESを返すようにしてください(デフォルトの戻り値はNO)。

  • 常にフォーカス可能なビューと、条件付きでフォーカス可能なビューの2種類あります。たとえばUIButtonオブジェクトは、無効にした場合、フォーカスできません。


  • preferredFocusedViewをオーバーライド。該当ビューがフォーカスされた際に、別のビュー(たとえばサブビュー)にフォーカスをリダイレクトすることができます。


  • didUpdateFocusInContext:withAnimationCoordinator:をオーバーライドし、フォーカスの更新に対して応答し、アプリケーションの内部状態を更新します。


フォーカスに付随するアニメーションを調整する

フォーカスを更新すると、関係する各ビューにアニメーションが表示されます。現在のビューはフォーカス状態に、これまでフォーカスされていたビューはフォーカス解除状態に、次に(更新後に)フォーカスされるビューはフォーカス状態に、それぞれ遷移するアニメーションです。しかし、アプリケーションで定義する通常のアニメーションと違って、UIKitはフォーカスに付随するアニメーションの再生速度やタイミング曲線を、システムが定義する所定の挙動になるよう調整します。たとえば、フォーカスが高速に移動する場合、ユーザーの動きに遅れないよう、アニメーションも速く再生します。

フォーカスに対応したUIKitのビュークラスには、システム定義のフォーカスアニメーションが組み込まれています。独自のアニメーションを組み込み、システムが定義する挙動をさせるためには、UIKitの組み込みクラスであるUIFocusAnimationCoordinatorと、addCoordinatedAnimations:completion:メソッドを使います。

コーディネータに追加したアニメーションは、フォーカスを当てる、または解除するアニメーション(いずれか一方)とともに再生されます。どちらになるかは、コーディネータに渡されるフォーカス環境に応じて決まります。フォーカスが当たるビューと解除されるビューの共通の祖先ビューの場合、フォーカスを当てるアニメーションになります。

一般にビューには、フォーカスが当てられるか解除されるかに応じて、別々のアニメーションがあります。ビューとアニメーションの対応関係は、ビューのdidUpdateFocusInContext:withAnimationCoordinator:メソッドをオーバーライドし、ビューの現在のフォーカス状態を表すコンテキストを調べることにより指定できます。didUpdateFocusInContext:withAnimationCoordinator:メソッドをオーバーライドするコード例は以下です。

- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context

withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
[super didUpdateFocusInContext:context withAnimationCoordinator:coordinator];
if (self == context.nextFocusedView) {
[coordinator addCoordinatedAnimations:^{
// focusing animations
} completion:^{
// completion
}];
} else if (self == context.previouslyFocusedView) {
[coordinator addCoordinatedAnimations:^{
// unfocusing animations
} completion:^{
// completion
}];
}
}

フォーカスを解除されるビュー、付与されるビューの。共通の祖先は、両方のビューを対象としてアニメーションを表示できます。たとえばUICollectionViewは、それまでフォーカスされていたセル、次にフォーカスされるセル療法について、アニメーションを表示できます。この場合、UICollectionViewCellのサブクラスを定義し、上に示したコード断片のようなアニメーション表示処理を実装すると良いでしょう。


フォーカスに関係する問題をデバッグする

UIKitには、アプリケーション実行中に生じる、フォーカスに関係する問題のデバッグ支援機能があります。


なぜこのビューはフォーカス可能でないのか?

フォーカス可能であるはずのビューがそうならない原因としては、次のような事項が考えられます。


  • ビューのcanBecomeFocusedメソッドがNOを返す。

  • ビューのhiddenプロパティの値がYESである。

  • ビューのalphaプロパティの値が0である。

  • ビューに対するユーザの対話操作が無効になっている。

  • ビューが別のビューの奥に隠れている。

UIKitのUIViewクラスには、_whyIsThisViewNotFocusableという隠しメソッドがあり、上述の事項を一括して検証できます。このメソッドは、あるビューの参照に対して、デバッガ内でのみ起動することになっています。原因と考えられる事項を、次のように人が読める形で出力します。

(lldb) po [(UIView *)0x148db5234 _whyIsThisViewNotFocusable]

ISSUE: This view has userInteractionEnabled set to NO. Views must allow user interaction to be focusable.
ISSUE: This view returns NO from -canBecomeFocused.

(lldb) po [(UIView *)0x14b644d70 _whyIsThisViewNotFocusable]

ISSUE: One or more ancestors are not eligible for focus, preventing this view from being focusable. Details:

<ExampleAncestorView 0x148db5810>:
ISSUE: This view has userInteractionEnabled set to NO. Views must allow user interaction to be focusable.


なぜフォーカスが想定外の場所に移動するのか?

フォーカスが期待する場所に移動しない、あるいはまったく動かないことがあります。フォーカスエンジンがどのように判断を下しているのか、その過程がわかれば解決に役立つかもしれません。

UIKitには、フォーカスエンジンが移動先ビューをどのように判断したか、その過程を視覚的に表し、UIFocusUpdateContextのインスタンスとして Quick Look に送信する機能があります。この画像は、shouldUpdateFocusInContext:またはdidUpdateFocusInContext:withAnimationCoordinator:にブレークポイントを設定すれば見ることができます。ブレークポイントに到達したら、デバッガでcontextパラメータを選択し、Quick Look を開いてください。

フォーカス移動中(更新中ではないことに注意)にブレークポイントに到達すると、Quick Look に現れる画像には、フォーカスエンジンが移動先ビューを検索した経路が現れています。以下の図にその例を示します。

この画像から次のような事項がわかります。


  • それまでフォーカスされていたビュー(検索の起点)。赤で表示。

  • 検索経路。赤の点線で表示。

  • 検索経路上に見つかった、フォーカス可能なUIView領域。紫で表示、パターン色でそれぞれを区別。

  • 検索経路上に見つかった、フォーカス可能なUIFocusGuide領域。青で表示、パターン色でそれぞれを区別。


ジェスチャーやボタンプレスの検出

UIKitのビューの多くは、リモコンのボタンプレスやタッチパッド上のジェスチャーに、適切に応答します。たとえばUIButtonオブジェクトは、フォーカスが当たっている状態でユーザーが選択ボタンを押した時にアクションメッセージを送信します。しかし、ボタンプレスやジェスチャーに応じて、独自のアクションを実行したい場合もあります。iOSでも、タッチイベントに応じて独自のアクションを実行することがありましたが、これと同様です。ボタンプレスイベントは、ほかのさまざまなイベントと同様、レスポンダチェーンをたどって処理させれます。UIGestureRecognizerクラスやUIResponderクラスには、リモコンのボタンを押した/離したときに応答する、新たなメソッドが加わっています。さらに、パンやスワイプを認識するジェスチャーリコグナイザも、Siri Remote のタッチパッド上のジェスチャーを、自動的に処理できるようになりました。


ジェスチャーリコグナイザを利用する

「タップ」ジェスチャーリコグナイザで、ボタンの押下を検出できます。通常、このリコグナイザが起動されるのは、「選択」ボタンを押した時です。allowedPressTypesプロパティを使って、どのボタンを押せばリコグナイザが起動するか、を指定できます。

以下に、「再生/一時停止」ボタンを押した時、ジェスチャーリコグナイザが起動されるようにするコード例を示します。

let tapRecognizer = UITapGestureRecognizer(target: self, action: "tapped:")

tapRecognizer.allowedPressTypes = [NSNumber(integer: UIPressType.PlayPause.rawValue)]
self.view.addGestureRecognizer(tapRecognizer)

同様に、スワイプやパンのジェスチャーリコグナイザを使って、リモコンのタッチパッド上の操作を検出できます。以下に、左から右へのジェスチャーを検出するコード例を示します。

let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: "swiped:")

swipeRecognizer.direction = .Right
self.view.addGestureRecognizer(swipeRecognizer)


低レベルのイベント処理

UIPressオブジェクトは、UITouchオブジェクトに似ていますが、リモコンや、ゲームコントローラなどその他のデバイスの、ボタンに関する情報を提供します。UIPressオブジェクトからは、どのボタンに関する情報か、及び、該当ボタンの状態(押した、離した、押したまま保持している、など)がわかります。アナログボタンであれば、UIPressオブジェクトからはさらに、該当ボタンにどの程度の圧力が加わっているか、もわかります。typeプロパティは、どの物理ボタンの状態が変化したか、を表します。UIPressオブジェクトはほかに、変化の具体的な状況を表すプロパティがあります。

UIGestureRecognizerオブジェクトやUIResponderオブジェクトには、プレスイベントが発生し、レスポンダチェーンをたどって届いた時に呼び出されるメソッド群を実装できます。こういったメソッドはいずれも、UIPressesEventオブジェクトを引数として受け取ります。イベントの他、状態が変わったボタンに関する情報を収容しています。以下に、選択イベントに応答する低レベル処理を、ビューコントローラに実装した例を示します。タッチイベントと同様、プレスイベントの処理を実装する場合、いずれか一つでもプレスメソッドハンドラを実装するならば、4つとも実装しなければなりません。

override func pressesBegan(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {

for item in presses {
if item.type == .Select {
self.view.backgroundColor = UIColor.greenColor()
}
}
}

override func pressesEnded(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
for item in presses {
if item.type == .Select {
self.view.backgroundColor = UIColor.whiteColor()
}
}
}

override func pressesChanged(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
// ignored
}

override func pressesCancelled(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
for item in presses {
if item.type == .Select {
self.view.backgroundColor = UIColor.whiteColor()
}
}
}


キーボード入力設計

Apple TV にはハードウェアとしてのキーボードがないため。キーボード入力は難しいことがあります。ユーザが操作を楽しめるよう、使い勝手に十分配慮する必要があります。できるだけテキスト入力しないくても済むよう、UI設計を工夫してください。Apple TV には、「標準」と「インライン」という2種類のキーボードがあります。


キーボード入力

UIAlertControllerUITextFieldを使って、キーボードの使い勝手をカスタマイズし、アプリケーションの特性に合わせたキーボードを作成できます。例えば電子メール作成に特化したキーボード、数字のみのキーボードなどです。UIAlertControllerUITextFiledはいずれも、UIKit/UITextInputTraits.hに記述されているオプションすべてに対応しています。inputAccessoryViewおよびinputAccessoryViewControllerに関係するAPIを使っても、キーボードの使い勝手をカスタマイズできます。


UIAlertControllerを使う

キーボードのUIはUIKitが完全に制御しますが、UIAlertControllerを使えばテキストフィールドやボタンをいくつでも追加可能です。また、キーボードの上部に、使い方の説明や見出しを表示できます。これを手前に出して使うのも容易です。しかし一般に、情報入力のためにリモコンをクリックする回数は、かなり多くなります。


UITextFieldを使う

UITextFieldを使って、キーボードを全画面に表示できます。キーボードに組み込まれた「Next」「Previous」ボタンで、テキストフィールド間のナビゲートが可能です。したがって、いったん(キーボードから)テキストフィールドに戻り、次のフィールドにフォーカスを移した後、もう一度クリックする、という操作は不要です。すべてのテキストフィールドを埋めると「Done」ボタンが現れ、前のページに戻れるようになります。開発者はこういったキーボードを、必要に応じてどこにでも配置できます。次/前のフィールドに移る機能も使えます。ただし、こういったキーボードの設定には、UIAlertControllerを使うよりも多くの作業が必要です。ビューを全て生成し、レイアウトしなければならないからです。


高度なUITextFieldキーボード

iOSのファーストレスポンダ機構は全て、tvOSでも働きます。したがって開発者は、可視か否かにかかわらずUIを表示し、becomeFirstResponderメソッドを使って、いずれかのテキストフィールドをファーストレスポンダとして設定できます。すると、該当テキストフィールドに対するアクションに応じて、キーボードのUIが現れるようになります。わざわざこのテキストフィールドにナビゲートし、クリックする必要はありません。ユーザーがキーボードから抜けるか「Done」ボタンを押すと、ファーストレスポンダのコールバックが呼び出されるので、以降はキーボード入力を受け付けない状態であることがわかります。


インラインのキーボード入力

インラインキーボードは、1行分の入力が可能な箇所に表示されます。UISearchControllerを使って、他社製のコンテンツに完全統合可能なキーボードを作成できます。ただし、UISearchControllerを用いる場合、カスタマイズの余地はほとんどありません。テキストフィールド自身にアクセスする、何らかの特性をカスタマイズする、入力アクセサリを追加する、といったことはできません。


ゲームコントローラの取り扱い

Apple TV には、iOSデバイスと同様、ゲームコントーラを接続できます。するとゲームコントローラは、フォーカスベースのユーザインターフェイスを制御するためにも使えるようになります。コントローラからの低レベル入力情報を、自動的に高レベルのイベントに変換し、レスポンダチェーンをたどって送信するようになっています。UIKitとフォーカスのやり取りにしか依存していないアプリケーションであれば、ゲームコントローラに対応するために何かする必要はありません。Apple TV Remote とゲームコントローラのどちらを使っているか意識しなくても済むよう、tvOSが透過的に処理します。

低レベルのコントローラ入力を取得して使いたい場合は、Game Controller フレームワークを使います。詳細については、「Game Controller Programming Guide」を参照してください。

Game Controller フレームワークには、Apple TV に対応するための大きな変更事項があります。


  • マイクロゲームパッドコントローラ用のプロファイル(GCMicroGamepad)が追加されました。Apple TV Remote の能力に特化したものです。

  • ビューコントローラのクラスとして、GCEventViewControllerが使用可能です。コントローラやリモコンの入力情報をアプリケーションに伝える経路制御に用います。


ゲームコントローラをサポートするゲームの要件

Appleは、ゲームコントローラをサポートするゲームで従うべき特別な要件を設定しています。これらの要件は、ゲームをいつでも楽しめるようにするためのものです。



  • ゲームは Apple TV Remote にも対応しなければなりません。コントローラがなくても遊べるようにするための要件です。


  • tvOS用のゲームがコントローラをサポートする場合、拡張コントロールレイアウトもサポートしなければなりません。tvOS用のコントローラは全て、非フォームフィット型の拡張コントローラです。


  • ゲームはスタンドアロン型コントローラを使用して進めることができなければなりません。拡張コントローラをサポートする場合、これ単独でも遊べるようにする必要があります。


  • 一時停止ボタンをサポートする必要があります。コントローラはすべて一時停止ボタンを備えています。ゲーム中に「Pause」ボタンを押せば、いつでも中断できるようにしなければなりません。メニュー画面を開いているなど、ゲーム中でなければ、「Pause」ボタンを押すと前の画面に戻ります。


コントローラの制御

Apple TV には、ゲームコントローラを同時に2台まで(及びリモコンを1台)接続できます。ゲームを設計する際には、この制限を頭に入れておかなければなりません。


Apple TV Remote をゲームコントローラとして使う

Apple TV Remote は、制限つきですが、ゲームコントローラとしても使えます。他のコントローラと同様、Game Controller フレームワーク内の、GCControllerオブジェクトを介して処理します。このリモコンはGCMotionGCMicroGamepadの両方のプロファイルに対応しています。マイクロゲームパッドのプロファイルは、Apple TV Remote でしか使えません。他のゲームコントローラにも対応するためには、拡張ゲームパッドのプロファイルも必要です。

リモコンをゲームコントローラとして使う場合、次のような特性があります。


  • リモコン上のタッチパッドは、十字キーとして使えます。十字キーからはアナログ入力データが得られます。

  • リモコンは縦横どちらの向きでも使えます。アプリケーションを開発する際には、ユーザがリモコンの向きを変えた時、プロファイルオブジェクトが自動的に入力データの向きを読み替えるかどうか、決めておく必要があります。

  • タッチパッドを強く押すと「A」ボタンとして働きます。

  • リモコンの「Play/Pause」ボタンは「X」ボタンとして働きます。

  • リモコンの「Menu」ボタンは、ゲームの一時停止ボタンとして働き、コントローラオブジェクトの「一時停止」ハンドラが呼び出されます。

  • リモコンは動きデータ(およびGCMotionプロファイル)に対応していますが、傾きや回転を認識することはできません。対応するプロパティの値は常に定数です。


コントローラ入力の送り先を決める

iOSの場合、ゲームコントローラのイベントは、Game Controller フレームワークを介して、デバイス上のタッチイベントと並行して受け取れます。一方、tvOS上のUIKitは、デフォルト状態の場合、低レベルのコントローラ入力を全て高レベルのイベントに変換し、レスポンダチェーンをたどって送られます。低レベルのコントローラ入力そのものは送られません。イベントを全てUIKitが処理してしまうからです。アプリケーション側でコントローラ入力を直接取得したければ、UIKitの変換処理を止める必要があります。以下に、コントローラ入力を送る2つの経路を示します。

ゲームは一般に、メインメニュー画面と、実際にゲームをする画面からなります。メニュー画面はUIKitの要素を使って、フォーカスベースの挙動になるよう実装します。ゲーム画面になったら、GameControllerフレームワークを使ってUIKitの変換処理を無効にし、コントローラ入力を直接読み取るようにします。

tvOSで、GameControllerフレームワークを使って低レベル入力を読み取る場合、GCEventViewController(またはそのサブクラス)を使ってゲーム画面を表示する必要があります。GCEventViewControllerオブジェクトのビューやサブビューがファーストレスポンダであれば、デフォルト状態の場合、ゲームコントローラ入力はまずビューコントローラが受け取り、GameControllerフレームワークを介してアプリケーション側に送ります。イベントがUIKitに送られることはありません。一方、ビューコントローラの階層を外れたビューがファーストレスポンダであれば、イベントは通常通り、UIKitが処理します。

レスポンダチェーン内にGCEventViewControllerオブジェクトがあれば、そのcontrollerUserInteractionEnabledプロパティを使ってイベントの送り先を切り替えることも可能です。たとえば、UIKitの要素で構成したメニュー画面に、ゲーム表示の一部であるMetalコンテンツが混在している場合、ゲームが一時停止状態になったら、controllerUserInteractionEnabledプロパティの値を切り替えて、UIKitにイベントを送信する必要があります。この場合、メニュー項目の中に、ゲームを再開するためのボタンが必要です。コントローラの一時停止ハンドラが呼び出されることはないからです。このボタンが押されたら、ゲームコントローラがイベントを処理する状態に戻した上で、ゲームを再開します。

何らかの理由で、GCEventViewControllerオブジェクトでイベントの伝搬方法を制御できない場合でも、この挙動を独自に実装することは可能です。そのためには、ビューまたはビューコントローラのpressesBegan:withEvent:メソッド(および押下イベントを処理する他のイベントハンドラ)おオーバーライドします。特定のイベントをUIKitに送りたい場合は、同じメソッドの、スーパークラス側の実装を呼び出します。逆に、GameControllerフレームワークのみで処理したければ、スーパークラス側を呼び出してはなりません。


パララックスアートワークの作成

アプリケーションアイコンには、レイヤード画像を作成する必要があります。フォーカス可能な他のUI要素に対しても、必要に応じて作成してください。UIKitのビューやフォーカス処理APIを使っている場合、適切な階層が指定されているUI要素にフォーカスを付与すると、自動的にパララックス効果が生じます。フォーカス可能なUI要素には、レイヤード画像が必要です。

UIKitは実行時に、2つの形式のレイヤード画像を扱えます。アセットカタログから読み込む形と、新たに追加されたレイヤード画像ファイル形式(.lsr)のファイルです。システム全体にわたり、他の画像形式を扱える箇所であれば、この2つの形式にも対応しています。たとえばGameCenterのアチーブメント画像はパララックス画像です。Xcodeまたは「Parallax Previewer」アプリケーションで、.lsrファイルを作成、エクスポートしてください。


「Parallax Previewer」アプリケーションでLSR画像を作成する

.lsr画像を作成、プレビューするアプリケーションをダウンロードしてください。以下に、「Parallax Previewer」アプリケーションの画面を示します。次の各部位から成ります。


  1. レイヤード画像の領域。画像を構成する各レイヤーに分かれています。

  2. 表示領域。各階層を合成して表示します。

  3. 表示サイズ。画像を表示する際のサイズを変更します。

  4. 位置領域。選択した画像階層の位置を調整します。

  5. 「+」「-」アイコン。画像階層を追加または削除します。

  6. サイズ領域。選択した画像階層のサイズを調整します。

  7. 再生ボタン。画像をアニメーション表示してパララックス効果を確認します。

  8. 「Apple TV icon」チェックボックス。オンにすると「Apple TV」アイコンに陰影効果が生じます。

  9. 「Background」。クリックすると表示領域の背景が変わります。

.lsr画像を作成する手順は次のとおりです。


  1. 画像の各階層に当たる、個々の.pngファイルまたは.jpegファイルをインポートします。

  2. 各階層のサイズや位置を適切に調整します。

  3. 「File」>「Export」>「LSR」コマンドを実行するとLSRファイルが生成されます。


XcodeないでLSR画像を作成する

xcodeでは、既存の.pngファイルをアプリケーションのアセットカタログ上にドラッグすることにより、画像スタックを作成できます。個々の.pngファイルが、パララックス画像の各階層になります。Xcodeウィンドウの右上隅にある「Export」ボタンを押すと、.lsr形式の視差画像をエクスポートできます。以下に、アプリケーションアイコン画像を作成している様子を示します。

また、既存の.lsrファイルを直接アセットカタログにドラッグすることにより、アプリケーションバンドルに含めることができます。プロジェクトのコンパイル時に、アセットカタログ内の画像スタックや.lsr画像を.carファイル形式に変換し、アプリケーションにバンドルするようになっています。


LCR画像を作成する

Xcodeは、.lsrファイルまたはアセットカタログを使って、パララックスファイルを指定します。このファイルを適切に処理し、アプリケーションバンドルに収容するようになっています。アプリケーションバンドル(やオンデマンドリソース)に収容されていない、任意のファイルを慈光寺に読み込みたければ、別途.lcrファイルに変換しておく必要があります。.lcr画像の作成には、Xcodeに付属のコマンドラインツール「layerutil」を使います。layerutilツールは、.lsrファイルを.lcrファイルに変換します。

「ターミナル」を起動して次のコマンドを実行すると、.lcr画像が生成されます。

xcrun --sdk appletvos layerutil --c <filename.lsr>

.lcr画像の名前は、(拡張子を除き)元のLSRファイルの名前と同じです。「--o <new_filename.lcr>」オプションを指定することにより、別の名前の.lcrファイルを生成することも可能です。


パララックス画像を読み込む

パララックス画像をアプリケーションに組み込む手順を以下に示します。



  1. UIImageオブジェクトを作成します。

  2. 画像を読み込む方法は、それがアプリケーションバンドル内にあるか、ダウンロードしたものか、によって異なります。


    • バンドル内にある場合:imageNamed:で読み込みます。

    • ダウンロードしたファイルの場合:imageWithContentsOfFile:で読み込みます。



  3. 読み込んだ画像を使ってUIImageViewオブジェクトを生成します。


  4. UIImageViewが別のビューの一部であれば、UIImageViewadjustsImageWhenAncestorFocusedYESとします。

ビューにフォーカスがあたると、パララックス画像が表示されます。


iCloudストレージ

Apple TV のストレージ容量にはリミットがあり、情報を保存しても、次にアプリケーションを開くときまで残っている保証はありません。さらに、複数のデバイス間でデータを共有するためには、いったん Apple TV 以外のどこかに格納する必要があります。Apple TV 用の共有ストレージとしては、iCloud Key-Value Storage (KVS)CloudKit の2種類があります。

少量(1MB以下)であれば iCloud KVS が便利です。デバイス全てにわたって、自動的に情報を同期するようになっています。ただし、このデータにアクセスできるのは、アプリケーションの所有者に限ります。それ以外のユーザはアクセスできません。詳しくは Designing for Key-Value Data in iCloud を参照してください。

大容量(1MB以上)のストレージが必要ならば、CloudKit を実装してください。ここに格納した情報は、他のユーザにアクセスさせることも可能です。あるユーザのアクションが、他のユーザにも影響を与えるような状況では、これが非常に有用です。例えばゲームでは、自分のターンのユーザが何らかのアクションをすると、他のユーザにもその影響が及びます。CloudKitをアプリケーションに実装する方法について詳しくは、CloudKit Quick Start を参照してください。


オンデマンドリソース

アプリケーションコンテンツは、普通にダウンロードしたアプリケーションバンドルに収容されていることもありますが、オンデマンドリソースはこれとは違い、App Store が別途、リクエストに応じて供給します。アプリケーションバンドルを小容量に抑え、高速にダウンロードできるようにするとともに、豊富な容量を提供できるという利点があります。アプリケーションがオンデマンドリソース群をリクエストすると。オペレーティングシステムが代わりにダウンロードし、保存します。アプリケーションはこのリソースを使った後、リクエストを解放します。いったんダウンロードしたリソースはデバイス上に残っていることがあり、その場合、初回の起動時よりも高速にアクセスできます。

Apple TV に格納できる各アプリケーションの容量は200MB以内です。これ以上になる場合、ダウンロード可能ないくつかのバンドルに分割しなければなりません。Xcodeでタグを作成し、これを必要なリソースに関連付けます。アプリケーションが、あるタグに関連付けられたリソースを要求すると、オペレーティングシステムは必要なアセットだけをダウンロードします。完了すれば、そのアセットはアプリケーション内で使えるようになります。

アセットは管理しやすいグループに分割してください。例えばゲームで、第5レベルに必要なアセットをまとめて1つのタグを与える、といった具合です。ダウンロード中である旨がユーザに伝わるよう、UIにも工夫が必要です。アプリケーションをテストして、ダウンロード可能なファイルサイズとして適切な値を調べてください。オンデマンドリソースの実装方法について詳しくは、「On-Demand Resources Guide」を参照してください。