ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
TVMLアプリをSwiftUIに移行
SwiftUIは、あらゆるAppleプラットフォームで優れたアプリを構築する際に役立ちます。tvOS 18でコンテンツをリビングルームで視聴できるようにするためのツールキットとして推奨されています。このセッションでは、広く利用されているTVMLKitのレイアウトとコントロールに準拠した環境をSwiftUIで構築する方法と、関連するヒントやベストプラクティスをご紹介します。
関連する章
- 0:00 - Introduction
- 3:54 - Lockups
- 5:12 - Shelves
- 7:35 - Catalogs
- 11:07 - Search
リソース
関連ビデオ
WWDC24
-
ダウンロード
こんにちは 「TVMLアプリをSwiftUIに移行」へようこそ エンジニアのJimです tvOS用のSwiftUIに携わっています これから皆さんに SwiftUIのtvOSアプリが持つ 優れた機能をご紹介し TVMLで開発されているような アプリを構築する方法について説明します tvOS 18のSwiftUIには セットトッププラットフォームに合わせて リッチなフル機能のメディアカタログと ストリーミングアプリを構築できる 各種機能が揃っています それと引き換えに TVMLKitは 公式に非推奨扱いとなりました TVMLKitは tvOSの一部として存続しますが プラットフォームとともに 発展することはありません 2015年に Appleが tvOSを世に出したとき 現在のような ストリーミングメディアの世界はなく 状況が大きく違いました 当時のストリーミングアプリは Webページ形式が普通で コンテンツプロバイダは Webの開発に力を注いでいました TVMLKitは 当時活用されていた リソースや専門知識を生かして 優れたApple TV用アプリを 開発するために適していたのです
現在 コンテンツの楽しみ方の主流は iPhoneやiPadなどのデバイス TVに接続したセットトップボックス コンピュータやApple Vision Proなどの デバイスに移りました ストリーミングメディアの カタログにアクセスする手段は 各種プラットフォームに適したツールで 作られたネイティブアプリです アプリは 親しみやすいパーソナルな カスタマー体験を実現しやすく デバイス環境に合った自然な 操作性と見せ方を守りながら 独自のスタイルを打ち出して 機能を提供できます SwiftUIのツールキットには 共通の言語とコンポーネントから 各プラットフォーム向けに 調整されたネイティブコードとUIを 生成できる機能があります お持ちの開発リソースや専門知識を 他のプラットフォームにも スムーズに変換して 機能させることができます
Apple TVであれば tvOSのネイティブUIが生成され 優れたtvOSアプリとしての特徴を備えた 美しいデザインになります TVMLKitでできたことは すべて SwiftUIでも実現されており さらなる柔軟性と 優れた新機能が年々加わります アプリ開発に使うSwiftは プラットフォーム共通の言語で iOS iPadOS macOS watchOS visionOSアプリの開発にも対応できます
つまり ネイティブ開発用の 既存のリソースを活用できます iPadアプリと同じ 開発ツールと手法を使って Apple TV用のアプリを 作成できるのです 同じコードを共用できる環境は 以前からありますが 他のOSとまったく同じコードで tvOSアプリを作れるようになっています
これから紹介するのは SwiftUIによる コンテンツカタログアプリ作成の基本です 共通のコンポーネントを使って 他のAppleプラットフォームと同じ方法で アプリを開発し すべてのApple TVユーザーが馴染んでいる インターフェイスと動作を実現します まず tvOS用メディアアプリで使われる 標準的なデザイン方針について説明します
普通 メディアカタログを起動したとき 初期画面に表示されるのは 視聴者向けホームページです ページの上部には通常 プロモーション用のコンテンツが割り当てられ 画面の端まで広がって表示されます
プロモーション用コンテンツには たいてい 即座にアクセスするための クイックアクションが付きます プロモーション用コンテンツエリアの下は その他のコンテンツを表示するシェルフです
各アイテムのアートワークを紹介する あざやかなロックアップが 並んで表示されます 最後に 画面上部にはタブバーがあり 検索機能など アプリ内のいろいろなセクションに 直接アクセスできます
ここでは 音楽やTVなどの Appleアプリでお馴染みの ロックアップを実現する 方法を説明します コンテンツロックアップの 流れるシェルフ表示の作成に役立つ テクニックを紹介します アプリのランディングページを 印象的なものにする 手軽なレイアウトの作り方や スムーズに使える検索機能を 数行のコーディングで 実現する方法も紹介します
では 先ほど見たような ロックアップデザインの作り方を 考えるところから始めましょう
tvOSで一般的に使われる 形のロックアップは 1枚の画像と その下の短いテキストで構成されます 画像の四隅には緩やかな丸みが付き フォーカスすると画像が持ち上がり 鏡を傾けたような反射効果の演出が入ります 持ち上がった画像の周りにあるテキストは 隠れないように位置調整されます SwiftUIでは これらすべてを実現できます 基本的なレイアウトは簡単に作れます 画像の下にテキストを配置するだけで 目指すデザインにかなり近いものになります これをインタラクティブにするために ボタン内に配置しましょう
tvOSのボタンは デフォルトでは 境界ありのbuttonStyleになります 背景のプラッターが付き フォーカスに反応して持ち上がり 色が変わります 目指すデザインにするには buttonStyleを境界なしに設定しましょう
この時点で 画像とタイトルは 正しい向きに揃っており 画像の四隅には丸みが付き かすかにドロップシャドウが付いています これがフォーカスされると 画像が手前に持ち上がり テキストが下にずれ 鏡面のようなハイライトが入ります ユーザーがリモコンのタッチサーフェスで 指を動かすと 画像の傾きが変わります このコードは そのまま 他のプラットフォームでも機能して プラットフォームに合った インタラクティブな反応を返します
コンテンツの基本的な ロックアップができました 次は シェルフの作成です コンテンツシェルフの 標準的なデザインを再確認して 作り方を考えてみましょう
コンテンツを左右に流すように表示し 画面のセーフエリアに収まるよう 自動的に位置を調整させます そうすれば 周りの要素との 整合性が保たれるでしょう
また はみ出したコンテンツを 画面のエッジから少し見せるようにして その方向に別のコンテンツが あることを伝えます
最初の段階として 水平スクロールのスタックを作ります 最も手軽にできるのは ScrollViewの中に LazyHStackを組み込む方法です 境界なしbuttonStyleで ロックアップの見た目を指定し ScrollViewの スクロールクリッピングをオフにします これは フォーカスされたロックアップの 大きい画像とドロップシャドウを断ち切らず ScrollViewの境界を越えて 表示するためです シェルフ内の各アイテムは Buttonとして表示します アイテムのポスター画像のサイズは 正確な値を直接指定することもできますが サイズから計算したaspectRatioだけを 指定したほうが便利な場合があります こうすると SwiftUIが最適な表示のために レイアウトを適宜調整できるからです ここに指定したaspectRatioは 映画ポスターの形に適した値ですが 実際の表示に使われるポイント寸法は 画面内のアイテム数に応じて決定されます
そのレイアウトの魔法を使うための指定が containerRelativeFrameモディファイアです SwiftUIに アイテムのフレームを 自動的に決定させるための指示を伝え その参考情報を提供しています すると セーフエリア内の インセットに収まるように コンテンツが自動調整され さらに スクロール境界からはみ出した 前後のアイテムを 画面の両端に部分的に表示するための 十分なスペースも確保されます
まず アイテムのフレームを決める 基準になるのは 直接の祖先であるコンテナビュー つまり ここでは ScrollViewの左右の境界線です これだけだと ボタンの画像が 大きく引き伸ばされ ScrollViewの幅いっぱいに表示されますが 2つの属性を追加して挙動を変えられます
これらの属性で コンテナの水平方向に 6個のアイテムを並べること さらに アイテム間に どれくらい余白をとるかを指定し 表示を調整しましょう これは 水平スタックに確保した スペースの大きさに合う設定です 両方の設定値を合わせるのは 重要なことです このコードは まったく変更せずに 他のプラットフォームでも使うことができ 間隔やアイテム数などが適宜調整された 同様のエクスペリエンスを提供できます
実際 短いコードを数行書いただけで tvOS 9でTVMLを使って作っていた Appleのメディアアプリと同様の 見た目や印象を再現できました このコンポーネントはとても柔軟なので わずかな変更を加えるだけで 多彩な目的に合った使い方ができます
たとえば ボタンのタイトルを削除し containerRelativeFrameモディファイアを 少し調整すると ヒーローサイズの ロックアップカルーセルができます
また アートワークの アスペクト比を変更し containerRelativeFrame 1個あたりの アイテム数を変更すれば アルバムアートワークに 最適なサイズになります
もっと情報密度を高める必要がある場合は カードbuttonStyleを使えば TVアプリの検索結果表示と同じタイプの ロックアップを作ることもできます
カードbuttonStyleの場合は 角が丸い台紙の上にコンテンツが表示され 持ち上がりや傾きの表現は 境界なしbuttonStyleよりも 控えめな動きになります 繰り返しますが そのような挙動に変更するために必要なのは カードbuttonStyleだけです
シェルフができたので スタートページの作成にかかりましょう
セクションビューでは シェルフごとにタイトルを表示し フォーカスがロックアップに移ったときは 自動的にタイトルの位置をずらして 隠れないようにします 各ボタンのタイトルと同様の挙動です
ホームビューでは 画面の隅々まで 背景画像を大きく表示したいところです ホームビューとしての存在感を 美しい画像でしっかりと示しましょう 最初は シンプルなビューを作って そこにタイトルと いくつかボタンを配置します 要素間には余白を作り 背景がよく見えるようにします
次は ここに背景を付けるため 外側のScrollViewに画像をアタッチします エリアを満たせるよう 画像のサイズを変更可能にします まだ周囲が空いています 画面の隅々まで広げるには セーフエリアを無視する 属性を設定します
ずいぶん格好よくなりました 目指していたとおり 画面の隅々までしっかりと コンテンツで満たされました とはいえ 下のシェルフアイテムの間にまで 背景が見えるのは 少し邪魔です
シンプルなグラデーションマスクで ボタンの背景を目立たなくしましょう それには プレーンな線形グラデーションを マスクモディファイア付きで適用するだけです フェードアウトした画像を通して 後ろにある背景が見えます
とても良くなりました アプリの完成版にかなり近い見た目です さらに理想をいえば 背景が注目の対象とならないときには 完全に隠れるようにしたいものです
tvOS 18.0では 新機能として ScrollView用のビューモディファイアが 多数加わりました それらを使えば ヘッダーセクションが画面外に出たら つまり ページの下方へスクロールしたら 背景の表示を消すことができます
ここで ヘッダービューに 新しいonScrollVisibilityChange モディファイアをアタッチして 画面外に出た場合の 状態変更の操作を追加します
次に 背景の表示処理を その状態に基づいて実行するようにします
また scrollTargetBehaviorを viewAlignedに設定して その状態遷移の 明確さを高めることにします
以上 すべてを盛り込むと このようなランディングページができます ダウンロード用のサンプルコードには より高度な他の方法で このスタイルのビューを実装する例も 含まれているので 参考にしてください tvOS 18の新しいScrollViewモディファイアと 対応リリースについて詳しくは 「Demystify SwiftUI containers」 をご覧ください
ここまで順調に説明してきましたが 以上の機能以外にも 優れたコンテンツ配信サービスには 検索機能が不可欠です 既に作ったものを生かし 優れた検索インターフェイスを 提供する方法を考えてみましょう まず何より 検索パネルにアクセスする 手段を提供しなくてはなりません
最も標準的なアプローチは TabViewを使う方法です tvOS 18では 表現力豊かなTabViewを 型安全な構文で作る新しい方法ができました これは 既存のコンテンツスタックを StackViewという新しいビュー内に配置し 新しいタブにSearchViewを 追加したものです SearchViewに含めるべき機能を 考えてみましょう
普通 検索ビューは 大量のコンテンツから 情報を探しやすいように作られます シェルフレイアウトは あまり適していると思えません 検索結果の表示には グリッドレイアウトを使うことにしましょう
最初の基本構造は 他のビューと同様ですが その中にLazyVGridを配置します
この例では4列構成 間隔は40ポイント サイズはフレキシブルに設定しています 先ほどの場合と同じく aspectRatioモディファイアを使って アイテムの実際のサイズは SwiftUIのレイアウト機構に 自動的に決定させるようにします
ロックアップ自体はButtonで もう見慣れたとおりですが 今度は上下方向にスクロールさせるので 縦軸上に多くの件数を収められるように 横長の画像を使って表示しましょう ここで 検索機能を提供するために 2つのしくみを追加します まず ビューに Stateプロパティを追加します 検索語句が入力されると このプロパティに検索語句が代入されます ForEachビューに渡すコンテンツを この値でフィルタリングすることで 検索結果の表示内容を絞り込みます
次に .searchableモディファイアを追加して Stateプロパティ searchTermにバインドします
検索ビューの表示は このモディファイア1つだけで実現できます アプリのユーザーによって 検索語句の入力が開始されたら Stateプロパティ searchTermの値に 一致しないアイテムが表示から除外されます
ユーザーは 好きなロボット植物学者の 出演する動画を検索したり 憧れの植物を検索したりと 何でも探す可能性があります 検索対象となるコンテンツが 大量にある場合や コンテンツアイテムに関連して 入力されるキーワードが 非常に長い場合のために 語句の続きを提案する機能があると 便利でしょう .searchSuggestionsモディファイアを 追加するだけで簡単に実現できます これは 現在の検索語句を含んだ キーワードのリストを取得し 一連のテキストビューを作成するコードです 結果はこのとおりです 表示された補完候補をユーザーが選択すると 検索フィールドに自動入力されます 補完候補にフォーカスすると 画面の検索フィールド内に プレビューが表示され その語句を選ぶことで実行される アクションと 現在の入力内容と補完候補との 一致状況が表示されます 繰り返しますが このコードは 他のプラットフォームでもそのまま使えます iPad Mac Apple Vision Pro用アプリにも まったく同じ方法で 検索フィールドを追加できます
立派なデザインのApple TV用アプリが SwiftUIではとても簡単に作れること そして ここで作り方を見てきたアプリが 他のAppleプラットフォームでも 実質的に無変更で動くことを 理解いただけたでしょう ネイティブのSwiftUIアプリを リビングルームに提供することが 今までになく簡単になったのです
私たちは明快で手軽な方法を用意して SwiftUIの最新のプラットフォーム機能を 採用していただけるよう努力しています ここで もう1つ 皆さんに紹介したいことがあります TVアプリのフローティングサイドバーが tvOS 18ではSwiftUIにより システム全体で利用できるようになりました トランスルーセントの 美しいデザインはそのままに TVアプリと同じインジケーターを コンパクトに表示できるよう縮小されています 独自開発のサイドバーをお使いの場合は この新しいデザインの採用を 検討してください 既存のナビゲーション分割ビューAPI を使って実現できます また 現在タブバーを採用している場合は わずか1行のコード変更で このデザインを今すぐ試すことができます tabViewStyleを sidebarAdaptableに設定してください 今までのタブバーが サイドバー表示に変わります このモディファイアは iPadOS 18でも同じように 新しいデザインのタブバーを iPad上に表示する場合に使うものです また ユーザーの操作で タブバーとサイドバーを 切り替えることができます tvOS上では 単なるサイドバーとして機能しますが わずか1行のコードで すべての関連動作が実現します
新しいTabView APIは あらゆる種類のコンテンツに対応でき タブバーのミニマルなデザインを保ちながら フル機能のサイドバーを実現したものです つまり iPadアプリでは シンプルで内容の少ないタブバーを表示し 展開表示によって サイドバーの充実した機能を提供できます tvOSでも 同じサイドバーコンテンツを 非常に美しく表示できます 詳しくは「Improve your tab and sidebar experience on iPad」で 新しいタブバーAPIの 詳細情報をご覧ください
Apple TV用アプリの提供を 検討中の方は 今こそ実行に移すときです SwiftUIなら 簡単に 立派な見栄えのtvOSアプリを開発でき しかも 大部分のコードを iPhone iPad Mac Apple Vision Proと共用できます また 既にお持ちのTVMLアプリがあるなら 今こそ 切り替えの検討をおすすめします
ここでは 優れたApple TV用アプリを SwiftUIで簡単に作れることを紹介しました ボーダーなしbuttonStyleには このプラットフォームで標準的な コンテンツロックアップとインタラクションに 必要な機能がすべて揃っています コンテナ相対フレームを使えば 試行錯誤なしで コンテンツシェルフを手軽に作れます 詳しくは「What’s new in SwiftUI」で tvOS 18の優れた新機能と 対応リリースについての情報をご覧ください 「Demystify SwiftUI containers」では 新しいコンテナAPIの詳細がわかります クロスプラットフォームの上質な アプリナビゲーションUIの実現方法は 「Improve your tab and sidebar experience on iPad」をご覧ください
このセッションの サンプルプロジェクトもご覧ください 完全に実装されたサンプルコードが 含まれているほか その他の例や提案事項も入っています また Apple TVでスムーズに動く クロスプラットフォームの フル機能アプリの実例として Destination Videoという 最高のサンプルプロジェクトがあります tvOS 18のApple TV向けに すばらしいアプリが公開されることを チーム一同楽しみにしています
-
-
4:18 - Borderless button lockup
Button {} label: { Image("discovery_landscape") .resizable() .frame(width: 250, height: 375) Text("Borderless Portrait") } .buttonStyle(.borderless)
-
5:38 - Standard content shelf
ScrollView(.horizontal) { LazyHStack(spacing: 40) { ForEach(Asset.allCases) { asset in Button {} label: { asset.portraitImage .resizable() .aspectRatio(250/375, contentMode: .fit) .containerRelativeFrame(.horizontal, count: 6, spacing: 40) Text(asset.title) } } } } .scrollClipDisabled() .buttonStyle(.borderless)
-
8:19 - Card button
ScrollView(.horizontal) { LazyHStack(spacing: 48) { ForEach(Asset.allCases) { asset in Button {} label: { HStack(alignment: .top, spacing: 10) { asset.landscapeImage .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 12)) VStack(alignment: .leading) { Text(asset.title) .font(.body) Text("Subtitle text goes here, limited to two lines") .font(.caption2) .foregroundStyle(.secondary) .lineLimit(2) Spacer(minLength: 0) HStack(spacing: 4) { ForEach(1..<4) { _ in Image(systemName: "ellipsis.rectangle.fill") } } .foregroundStyle(.secondary) } } .padding([.leading, .top, .bottom], 12) .padding(.trailing, 20) .frame(maxWidth: .infinity) } .containerRelativeFrame(.horizontal, count: 3, spacing: 48) } } } .scrollClipDisabled() .buttonStyle(.card)
-
8:39 - Landing page
ScrollView(.vertical) { LazyVStack(alignment: .leading, spacing: 26) { VStack(alignment: .leading) { Text("tvOS with SwiftUI") .font(.largeTitle).bold() Spacer(minLength: 300) HStack { Button("Show") {} Button(“More Info…”) {} Spacer() } .padding(.bottom, 100) Spacer() } .onScrollVisibilityChange { visible in withAnimation { belowFold = !visible } } Section("Movie Shelf") { MovieShelf() } Section("TV and Music Shelf") { TVMusicShelf() } Section("Content Cards") { CardShelf() } } .scrollTargetLayout() } .scrollClipDisabled() .background(alignment: .top) { if !belowFold { Image("beach_landscape") .resizable() .aspectRatio(contentMode: .fill) .ignoresSafeArea() .mask { LinearGradient(stops: [ .init(color: .black, location: 0.0), .init(color: .black, location: 0.45), .init(color: .black.opacity(0), location: 0.8) ], startPoint: .top, endPoint: .bottom) } } } .scrollTargetBehavior(.viewAligned)
-
11:13 - Tab view
TabView { Tab("Stack", systemImage: "line.3.horizontal") { StackView() } // Other Tabs... Tab("Search", systemImage: "magnifyingglass") { SearchView() } }
-
11:50 - Search page
@State var searchTerm: String = "" let columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: 40), count: 4) ScrollView(.vertical) { LazyVGrid(columns: columns) { ForEach(sortedMatchingAssets) { asset in Button {} label: { asset.landscapeImage .resizable() .aspectRatio(16 / 9, contentMode: .fit) Text(asset.title) } } } } .scrollClipDisabled() .buttonStyle(.borderless) .searchable(text: $searchTerm) .searchSuggestions { ForEach(suggestedSearchTerms, id: \.self) { suggestion in Text(suggestion) } }
-
14:59 - Sidebar adaptable tab view style
TabView { Tab("Stack", systemImage: "line.3.horizontal") { StackView() } // Other Tabs... Tab("Search", systemImage: "magnifyingglass") { SearchView() } } .tabViewStyle(.sidebarAdaptable)
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。