Highlight
tvOS 18 正式弃用 TVMLKit,SwiftUI 现已覆盖其全部能力,一行
.tabViewStyle(.sidebarAdaptable)即可将 Tab 栏变为浮动侧边栏。
核心内容
2015 年 Apple 发布 tvOS 时,流媒体 app 几乎都是网页实现的。开发团队的技术栈和人力集中在 Web 端,原生开发对他们来说门槛很高。TVMLKit 就是 Apple 为这些团队提供的桥梁——用类似 HTML 的模板语言(TVML)描述界面,再由 JavaScript 驱动逻辑,就能在 Apple TV 上跑起来。
9 年后的今天,流媒体生态已经完全不同。用户在 iPhone、iPad、Mac、Vision Pro 上通过原生 app 消费内容,SwiftUI 成了跨平台构建原生 UI 的统一方案。Apple 在 tvOS 18 中正式宣布弃用 TVMLKit——它仍然可用,但不会再获得任何新功能。如果你的 app 还依赖 TVMLKit,现在就是开始迁移的时候。
SwiftUI 在 tvOS 18 已经能覆盖 TVMLKit 的全部能力,并且每年持续增加新功能。你用 Swift 编写代码,这是所有 Apple 平台通用的语言;你用 SwiftUI 构建视图,同一套组件在 iOS、iPadOS、tvOS 上都能运行。Session 从一个典型的 tvOS 媒体 app 入手,逐步演示了如何用 SwiftUI 实现内容锁屏(lockup)、水平货架(shelf)、首页促销布局、搜索界面,以及新的浮动侧边栏——这些正是 TVMLKit 过去做的事,现在 SwiftUI 做得更好。
详细内容
内容锁屏(Lockup):borderless buttonStyle
tvOS 上最常见的锁屏布局是图片浮在文字上方,图片带圆角,聚焦时抬起、倾斜、出现高光,文字自动下移避免被遮挡。SwiftUI 只需要一个 .borderless buttonStyle 就能实现全部效果(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
内容货架需要水平滚动、对齐安全区域、边缘露出下一项内容。核心是 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)让每行放 6 个元素,自动对齐安全区域并让屏幕外内容露出spacing: 40必须和LazyHStack的spacing: 40一致,否则对齐会出错.scrollClipDisabled()必须加——聚焦抬起效果会让按钮超出自身 bounds,不加会被 ScrollView 裁剪
Card buttonStyle:信息密度更高的锁屏
搜索结果等场景需要展示更多文字信息,用 .card buttonStyle 可以获得带圆角背景托盘的锁屏,抬起效果更克制(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让每行 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 只需两个修饰符就能实现完整的搜索体验(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提供补全建议,选中后自动填入搜索框LazyVGrid四列布局,横向排更多内容,适合搜索结果的信息密度
侧边栏:一行代码切换
tvOS 18 引入了系统级的浮动侧边栏,外观和 TV app 里的一样。如果你的 app 已经用了 TabView,只需一行代码就能切换(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 重建内容锁屏,用containerRelativeFrame重建货架,逐步替换。 - 做什么:复用跨平台代码构建 tvOS app。为什么值得做:同一套 SwiftUI 视图在 iOS、iPadOS、tvOS 上都能运行,开发资源投入一份代码覆盖多个平台。怎么开始:抽取与平台无关的视图组件(如锁屏、货架),通过条件编译或布局参数适配不同屏幕尺寸。
- 做什么:采用
.tabViewStyle(.sidebarAdaptable)替换自定义侧边栏。为什么值得做:系统级浮动侧边栏有一致的交互体验、半透明毛玻璃效果和紧凑指示器,且与 iPadOS 18 的 Tab 栏 API 共享同一套代码。怎么开始:将现有TabView加上.tabViewStyle(.sidebarAdaptable)修饰符,验证在各平台上的表现。 - 做什么:用
.searchable+.searchSuggestions实现搜索功能。为什么值得做:两个修饰符就完成搜索框、键盘、补全建议的全部交互,代码量远少于手写实现。怎么开始:给搜索视图添加@State持有搜索词,绑定到.searchable,用搜索词过滤数据源,再用.searchSuggestions提供补全。
关联 Session
- 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