ハイライト
tvOS 18 で TVMLKit が正式に非推奨となり、SwiftUI がその全機能をカバーします。
.tabViewStyle(.sidebarAdaptable)1 行で Tab バーをフローティングサイドバーに切り替えられます。
主要内容
2015 年に Apple が tvOS をリリースしたとき、ストリーミング App のほとんどは Web 実装でした。開発チームの技術スタックと人材は Web 側に集中し、ネイティブ開発のハードルは高くありました。TVMLKit はそうしたチーム向けの架け橋——HTML に似たテンプレート言語(TVML)で UI を記述し、JavaScript でロジックを駆動すれば Apple TV 上で動きます。
9 年後の今日、ストリーミングエコシステムは大きく変わりました。ユーザーは iPhone、iPad、Mac、Vision Pro のネイティブ App でコンテンツを消費し、SwiftUI がクロスプラットフォームのネイティブ UI 構築の統一手段です。Apple は tvOS 18 で TVMLKit を正式に非推奨としました——引き続き利用可能ですが、新機能は追加されません。App が TVMLKit に依存しているなら、今が移行を始めるタイミングです。
tvOS 18 の SwiftUI は TVMLKit の全能力をカバーし、毎年新機能が追加されます。Swift でコードを書き、すべての Apple プラットフォーム共通の言語を使います。SwiftUI でビューを構築すれば、同じコンポーネントが iOS、iPadOS、tvOS で動作します。セッションは典型的な tvOS メディア App から始め、コンテンツ lockup、水平 shelf、ホームプロモレイアウト、検索 UI、新しいフローティングサイドバーを SwiftUI で段階的に実装する方法を示します——TVMLKit が担っていたことを、SwiftUI がより良く実現します。
詳細
コンテンツ lockup:borderless buttonStyle
tvOS で最も一般的な lockup レイアウトは、画像がテキストの上に浮き、角丸付きで、フォーカス時に持ち上がり、傾き、ハイライトが現れ、テキストが自動的に下に移動して遮られないようにします。SwiftUI では .borderless buttonStyle 1 つで全効果を実現(04:18)。
Button {} label: {
Image("discovery_landscape")
.resizable()
.frame(width: 250, height: 375)
Text("Borderless Portrait")
}
.buttonStyle(.borderless)
キーポイント:
Buttonで画像とテキストをラップ。tvOS のデフォルト buttonStyle は.bordered(背景トレイ付き)——.borderlessにすると画像が自由に浮く- フォーカス時に画像が自動的に持ち上がり、傾き、鏡面ハイライトが現れ、テキストが下に移動——すべてシステムが処理
- このコードは iOS/iPadOS でも直接実行可能。インタラクションはプラットフォームに自動適応
水平 shelf:containerRelativeFrame
コンテンツ shelf には水平スクロール、セーフエリアへの整列、次の項目の端の露出が必要。中核は containerRelativeFrame 修飾子(05:38)。
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)
キーポイント:
LazyHStackとScrollView(.horizontal)で水平スクロール、spacing: 40で項目間隔を制御- 固定サイズの代わりに
aspectRatioを使い、SwiftUI が画面幅に応じて自動調整 containerRelativeFrame(.horizontal, count: 6, spacing: 40)で 1 行 6 要素、セーフエリア整列、画面外コンテンツの露出spacing: 40はLazyHStackのspacing: 40と一致必須、不一致だと整列が崩れる.scrollClipDisabled()は必須——フォーカス時の持ち上げでボタンが bounds を超え、ScrollView でクリップされる
Card buttonStyle:情報密度の高い lockup
検索結果など、より多くのテキスト情報が必要なシーンでは .card buttonStyle で角丸背景トレイ付き lockup を取得。持ち上げ効果はより控えめ(08:19)。
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)
.buttonStyle(.card)
キーポイント:
.cardbuttonStyle は角丸背景トレイを提供。持ち上げと移動が穏やかで、情報密度の高いレイアウトに適するHStackは左画像・右テキスト。左画像はclipShapeで角丸- 右
VStackにタイトル、サブタイトル、アイコン行を積み重ね。サブタイトルは.lineLimit(2)で行数制限 containerRelativeFrameのcount: 3で 1 行 3 カード、spacing: 48は外側と一致
ホームプロモレイアウトと onScrollVisibilityChange
ホーム上部には画面いっぱいの大画像をプロモ背景にし、下にスクロールすると画像がフェードアウト。tvOS 18 で onScrollVisibilityChange 修飾子が追加(08:39)。
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)
キーポイント:
.onScrollVisibilityChangeは tvOS 18 の新修飾子。header が表示領域外にスクロールするとコールバックbelowFold状態が背景画像の表示/非表示を制御。withAnimationでスムーズな遷移- 背景
Imageは.ignoresSafeArea()で画面いっぱい、LinearGradientmask で下部をグラデーション消去 .scrollTargetBehavior(.viewAligned)でスクロールがビュー境界に整列、遷移がクリーン
検索:searchable + searchSuggestions
検索はストリーミング App の必須機能。SwiftUI では 2 つの修飾子で完全な検集体験を実現(11:50)。
@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)
}
}
キーポイント:
@State var searchTermを.searchable(text: $searchTerm)にバインド。入力内容が自動同期searchTermでForEachのデータソースをフィルタして結果を絞り込み.searchSuggestionsで補完提案。選択すると検索欄に自動入力LazyVGrid4 列レイアウトで横方向により多くのコンテンツ。検索結果の情報密度に適する
サイドバー:1 行で切り替え
tvOS 18 で TV App と同じシステムレベルのフローティングサイドバーが導入。既に TabView を使っているなら 1 行で切り替え可能(14:59)。
TabView {
Tab("Stack", systemImage: "line.3.horizontal") {
StackView()
}
Tab("Search", systemImage: "magnifyingglass") {
SearchView()
}
}
.tabViewStyle(.sidebarAdaptable)
キーポイント:
.tabViewStyle(.sidebarAdaptable)で Tab バーがフローティングサイドバーに。半透明ブラー効果、折りたたむとコンパクトインジケーター- tvOS 18 の新
Tab構文が旧TabView+.tabItemを置換。型安全で拡張可能 - 同じ修飾子を iPadOS 18 でも Tab バーとサイドバーの切り替えに使える
重要ポイント
- やること:既存 TVMLKit App を SwiftUI に移行。理由:TVMLKit は正式に非推奨で新機能なし。SwiftUI は毎年プラットフォーム更新あり。始め方:最もシンプルなページから。
.borderlessbuttonStyle で lockup を再構築、containerRelativeFrameで shelf を再構築し、段階的に置き換え。 - やること:クロスプラットフォームコードを再利用して tvOS App を構築。理由:同じ SwiftUI ビューが iOS、iPadOS、tvOS で動作。1 つのコード投資で複数プラットフォームをカバー。始め方:プラットフォーム非依存のビューコンポーネント(lockup、shelf)を抽出し、条件コンパイルやレイアウトパラメータで画面サイズに適応。
- やること:
.tabViewStyle(.sidebarAdaptable)でカスタムサイドバーを置換。理由:システムのフローティングサイドバーは一貫したインタラクション、半透明ブラー、コンパクトインジケーターを提供。iPadOS 18 の Tab バー API とコード共有。始め方:既存TabViewに.tabViewStyle(.sidebarAdaptable)を追加し、各プラットフォームで検証。 - やること:
.searchable+.searchSuggestionsで検索機能を実装。理由:2 つの修飾子で検索欄、キーボード、補完提案の全インタラクションを完了。手書きよりはるかに少ないコード。始め方:@Stateで検索語を保持、.searchableにバインド、データソースをフィルタ、.searchSuggestionsで補完を提供。
関連セッション
- Demystify SwiftUI containers — tvOS 18 の新 ScrollView コンテナ修飾子を深掘り
- What’s new in SwiftUI — tvOS 18 および全系プラットフォームの SwiftUI 新機能総覧
- Elevate your tab and sidebar experience in iPadOS — 新 Tab バーとサイドバー API 詳解、sidebarAdaptable スタイル含む
- SwiftUI essentials — SwiftUI 基礎概念とクロスプラットフォーム構築の要点
コメント
GitHub Issues · utterances