大多数浏览器和
Developer App 均支持流媒体播放。
-
将 TVML App 迁移到 SwiftUI
SwiftUI 可帮助你在所有 Apple 平台上打造出色的 App,更是利用 Apple tvOS 18 将你的内容引入客厅环境的首选工具包。了解如何使用 SwiftUI 通过 TVMLKit 创建熟悉的布局和控件,并获取相关技巧和推荐做法。
章节
- 0:00 - Introduction
- 3:54 - Lockups
- 5:12 - Shelves
- 7:35 - Catalogs
- 11:07 - Search
资源
相关视频
WWDC24
-
下载
大家好 欢迎观看 “将 TVML App 迁移到 SwiftUI” 我叫 Jim 是 SwiftUI for Apple tvOS 团队的工程师 我很高兴能向大家展示 SwiftUI 中适用于 Apple tvOS App 的出色功能 并介绍如何使用它构建 以前使用 TVML 构建的 App 在 Apple tvOS 18 中 SwiftUI 拥有为你喜爱的机顶盒平台 构建丰富多样、功能齐全的 媒体目录和流媒体 App 所需要的一切 正是因为这样 Apple 正式弃用 TVMLKit 它仍然在 Apple tvOS 中提供 但不会再随平台更新 Apple 在 2015 年 推出 Apple tvOS 当时的流媒体与今天的流媒体 相去甚远 流媒体 App 基本上 以网页形式实现 内容提供商的开发资源 也以网页为中心 Apple 提供了 TVMLKit 用于利用这些资源 和专业知识为 Apple TV 打造出色的 App
如今我们在各种设备上消费内容 比如 iPhone 或 iPad、 使用机顶盒的电视机、 电脑以及 Apple Vision Pro 等设备 我们通过原生 App 访问流媒体目录 这些 App 是使用专为这些平台 量身定制的工具构建的 这些 App 可为顾客提供 更熟悉、更个性化的体验 既能展现自己独特的风格又能 保持这些设备流畅的互动体验 和展示风格 SwiftUI 提供了一个工具包 可针对这些平台量身打造 原生代码和 UI 全部使用同一种语言 采用相同的组件构建 其中使用的开发资源和专业知识 可以清晰地转换为任何其他平台
在 Apple TV 上 它生成原生的 Apple tvOS UI 各种设计线索旨在打造 出色的 Apple tvOS App TVMLKit 能做的事现在都可以 在 SwiftUI 中实现 灵活性更高 并且不断推出新功能 App 代码使用 Swift 编写 这种语言也用于开发 iOS、iPadOS、macOS、watchOS 和 visionOS 上的 App
因此它能够充分利用现有的 原生开发资源 为 iPad 开发 App 而使用 的工具和技术 也可用于为 Apple TV 开发 App 现在你还可以轻松地 将其他 App 的代码 直接用于开发 Apple tvOS App
我将介绍在 SwiftUI 中 搭建内容目录 App 的基础做法 你将了解到 用于为任何 Apple 平台 构建 App 的相同组件 可用于创建 Apple TV 的 所有用户都会立即感到很熟悉的 界面和行为 首先我们来了解 Apple tvOS 媒体 App 的典型设计语言
打开媒体目录时 一般首先显示主页 主页针对观看者进行定制 页面的上半部分通常 显示一些推广内容 漂浮在屏幕边缘
这些推广内容通常 提供一些快速操作 以方便用户快速访问 推广内容区域下方 显示货架中的更多内容
每个项目都显示为 一个色彩缤纷的动态组合 其中展示项目的插图 最后 标签页栏位于屏幕顶部 可让用户直接访问 App 的不同部分 包括搜索功能
在这个视频中 我将介绍如何 构建 Apple Music 和 Apple TV 等 App 使用的类似组合 我将介绍一些有用的技巧 来构建这些内容组合 的悬浮货架 我将介绍一种简单的方式 来为 App 的着陆页面 设计令人印象深刻的布局 并展示如何仅用几行代码 就能打造流畅的搜索体验
首先我们来了解如何进行 之前提到的 组合设计
Apple tvOS 上最常见的组合类型 是一张图片 悬浮在文字上方 图片具有圆润的边角 被选中时 会升起并微微转动 显示镜面反光效果 旁边的文字会下滑 以避免被图片遮挡 这些效果都可以使用 SwiftUI 来实现 基本布局很容易实现 图片位于文字上方 这已经很接近我们的设计 但我们希望它是交互式的 所以把它放到按钮中
Apple tvOS 中的按钮默认使用 有边框的 buttonStyle 它们会获得一个背景盘 在获得焦点时会升起并变色 为实现我们想要的外观 我们将使用无边框 buttonStyle
现在图片和标题方向正确 图片具有圆角和轻微的投影 获得焦点时 图像升起 文字下移 然后显示镜面反光效果 当手指在遥控器的触控表面上 移动时 整个图片会微微转动 这段代码在其他平台上 也按原样运行 并采用相应平台特有的交互机制
我们已完成基本的内容组合 可以继续创建货架 我们来回顾一下 内容货架的标准外观 以确定想要实现的效果
我们的内容应该水平滚动 并且应该与 屏幕的安全区域边界对齐 这样可以使它与周围的 元素保持协调
它还应该允许屏幕外的内容 在屏幕边缘微露 来表示这个方向有更多内容
我们使用水平滚动的堆栈 来开始构建 理想情况下 这应是嵌入 ScrollView 中的 LazyHStack 我们将使用无边框 buttonStyle 来获取组合 并确保停用 ScrollView 上的 滚动修剪 这样获得焦点的组合就可以 变大并把投影投射到 ScrollView 本身的边界之外 货架中的每个项目 都将由一个按钮表示 虽然你可以直接指定某个项目的 海报图像的确切大小 但使用根据这个大小计算出的 aspectRatio 可能效果更好 这样 SwiftUI 就可以进行调整 为你提供理想的布局 在这个例子中 我的 aspectRatio 提供电影海报的形状 而实际的点尺寸将由 屏幕上显示的项目数决定
布局的魔法发生在 containerRelativeFrame 修饰符中 这个修饰符告诉 SwiftUI 它应该 为我们确定这个项目的边框 并提供了一些有用的信息 这会自动调整内容 使它与前面 和后面的安全区域嵌入对齐 并为当前滚动横幅之外 的项目提供足够的空间 这些额外项目会在屏幕的两侧微露
首先 项目的边框应取决于 最近的父级容器视图的水平边界 在这个例子中为 ScrollView 单独使用这个视图会让这个 按钮的图像拉伸到 ScrollView 的边界 但还有 另外两个属性可以更改这个行为
在这里 我们要求在容器的宽度内 有六个项目 我们提供了一点帮助 说明了项目之间相隔的 的空间大小 这个值与提供给水平堆栈 的间距值匹配 这两个值必须相同 同样的 你可以使用完全相同的 代码在其他平台上 打造类似的体验 只需要调整 间距和项目数量等
只需几行简短的代码 你现在就复制了 Apple 自身的 媒体 App 的外观和使用体验 就像 在 Apple tvOS 9 中使用 TVML 一样 这是一个非常灵活的组件 可通过多种方式使用 只需进行非常小的改动
例如 移除按钮标题并稍微调整 containerRelativeFrame 修饰符 你会得到题图大小的 组合的轮播界面
更改插图的宽高比 和每个 containerRelativeFrame 的项目数量 你会得到适合专辑封面的完美尺寸
如果你需要更高的信息密度 可以使用 .card buttonStyle 来构建与 TV App 中的搜索结果 类型相同的组合
卡片 buttonStyle 提供 圆角背景盘来支撑你的内容 它的升起和移动幅度 比无边框 buttonStyle 更小 同样 你只需要附加 .card buttonStyle 即可实现这一行为
有了货架之后 我们可以开始组装起始页面
分组视图会为每个货架提供标题 当组合获得焦点时 标题会自动移开 以避免被遮挡 就像按钮上的标题一样
为了正确引入主页视图 我们希望宽屏背景图像 填充屏幕的每个边缘 我们可以首先添加 一个简单视图 包含标题 和一些按钮 按钮之间有一定的间距 以显示背景图像
现在我们可以添加一个背景 将它附加到外层 ScrollView 即可 我们将确保图片 可根据需要调整大小 但它不会扩展到屏幕边缘 忽略安全区域就可以 快速解决这个问题
这样看起来已经好多了 内容一直显示到显示屏边缘 就像我们想要的那样 但它显示在货架后面有点让人分心
简单的渐变遮罩可以 使按钮后的图片淡出 你只需要应用线性渐变 和遮罩修饰符 现在图片淡出 显示它后面的背景
这样看起来好多了 很接近我们最终的应用程序 理想情况下 我们希望 当没有获得焦点时 图像会完全消失
Apple tvOS 18.0 中新增了许多 专用于 ScrollView 的视图修饰符 我们可以利用这些修饰符 来移除背景图像 适用的情况是标题部分 滚动到屏幕之外 以及内容移动 belowFold
这里我将 onScrollVisibilityChange 修饰符 附加到标题视图 当它移出屏幕时 我将更改一些状态
然后我选择基于状态显示背景
我还将 scrollTargetBehavior 设置为 viewAligned 以便让过渡更明确
将所有这些组合到一起 我们就得到了着陆页面 你可以下载本视频的示例代码 以获得一些其他示例 了解如何使用更高级的方法 实现这种视图样式 要进一步了解 Apple tvOS 18 和 一致版本中新的 ScrollView 修饰符 请观看“解密 SwiftUI 容器”
到目前为止一切都很棒 但还有一项功能 是出色的内容交付服务必备的 这就是搜索功能 下面我们来了解如何利用 已经构建好的东西 来快速提供出色的搜索界面 在这之前 你需要提供 访问搜索窗格的方法
典型的做法是使用 TabView 还有一个新的 表达能力强的类型安全语法 用于 在 Apple tvOS 18 中组装 TabView 我将现有内容堆栈放入 名为 StackView 的新视图中 并已将 SearchView 添加到新标签页 让我们来看看 SearchView 中应该有什么
搜索视图通常允许 同时在大量内容中 进行搜索 所以货架布局好像不太合适 我们将使用网格布局来显示结果
我们从与其他视图相同的 基本结构开始 但在其中放入一个 LazyVGrid
我将它设置为创建四列 间距为 40 点且大小灵活 同样 我将让宽高比修饰符 和 SwiftUI 的布局机制来确定 项目的实际大小
组合本身是熟悉的按钮 但它现在是垂直滚动 我使用了一张横向图像 以沿这个轴显示更多内容 为了使这些内容可搜索 我将添加两个东西 首先我将向视图添加 state 属性 这将会更新 从而包含 正在输入的任何搜索词 我们可以用它来筛选 传递到 for-each 视图的内容 从而缩小在结果中显示的内容范围
接下来 我将添加 .searchable 修饰符 向它传递 searchTerm 状态属性的绑定
呈现搜索视图只需要这一个修饰符 在这里 App 用户可以键入搜索词 你可以使用 搜索词状态属性来 滤除任何不匹配的项目
用户可以搜索包含他们最喜欢的 机器人植物学家的视频 也可以直接挥洒自己的植物学灵感 你可能有很多内容可供用户搜索 你的内容项目的一些 潜在搜索词和关键词 可能会很长 如果能提供一些补全建议 会很有帮助 这也很容易做到 只需添加 .searchSuggestions 修饰符即可 这里有一个包含当前搜索词 的关键词列表 我将根据这个列表创建 一系列文本视图 结果如下 选择任何一个补全文本 都会将它的值 自动放入搜索栏中 选中某一个建议 会在搜索栏内提供屏幕预览 表明操作和建议都会执行 以及这个词与当前输入的匹配程度 同样 这些代码也可以在 其他平台上使用 可通过同样的方法向 iPad、Mac 或 Apple Vision Pro App 添加搜索栏
现在你应该清楚如何 轻松使用 SwiftUI 为 Apple TV 构建精美的 App 以及我们所构建的所有东西 都可以在 Apple 的其他平台上运行 几乎不需要更改 将原生 SwiftUI App 带到客厅环境 从未如此简单
我们致力于提供简洁明了的方法 从 SwiftUI 采用 最新的平台功能 我还想向你们展示一个功能 TV App 中的悬浮式边栏 这项功能现已推出 可通过 SwiftUI 在 Apple tvOS 18 中使用 它同样拥有美丽的半透明外观 并且可以收起 以显示 TV App 中的紧凑指示图标 如果你在构建自己的边栏 不妨考虑在你的 App 中采用 这个新的外观 你可使用现有的导航分屏浏览 API 来构建它 如果你的 App 目前使用标签页栏 你可以立即试用这个外观 只需使用一行代码 只需将 tabViewStyle 设置为 sidebarAdaptable 标签页栏就会显示为边栏 你也可以在 iPad 上 使用这个修饰符 在 iPadOS 18 中 获得新的标签页栏外观 它还允许用户在标签页栏 和边栏之间互相切换 在 Apple tvOS 中 它提供边栏和所有相关行为 只需要一行代码
新的 TabView API 支持 各种不同类型的内容 你可以构建功能完备的边栏 同时保持最小标签页栏外观 这意味着 iPad App 可以采用 简约的标签页栏形式 并可扩展为包含详细信息 和丰富功能的边栏 同样的边栏内容 在 Apple tvOS 上看起来也很棒 你可以观看“提升 iPad 中的 标签页和边栏使用体验” 详细了解新的标签页栏 API
如果你想要在 Apple TV 上构建 App 现在就是最好的时机 SwiftUI 可帮助你轻松构建 Apple tvOS App 它们拥有精美的外观 并且大部分代码可以用于 iPhone、iPad、Mac 和 Apple Vision Pro App 如果你目前有 TVML App 现在是时候考虑做出改变了
你已了解如何轻松使用 SwiftUI 构建出色的 Apple TV App 无边框 buttonStyle 提供 获取平台标准内容组合外观 和交互所需要的一切 使用容器相对框架可以消除 搭建内容货架时的所有臆测 请观看“SwiftUI 的新功能” 了解 Apple tvOS 18 和 一致版本中强大的新功能 要进一步了解新的容器 API 请观看“解密 SwiftUI 容器” 要了解如何打造出色的 跨平台顶层 App 导航 UI 请观看“提升 iPad 中的 标签页和边栏使用体验”
查看本视频中的示例项目 获取完整的示例代码 以及更多示例和建议 要查看一个功能全面的 跨平台 App 的示例 它在 Apple TV 上表现出色 不妨查看 Destination Video 示例项目 SwiftUI for Apple tvOS 团队 期待看到你构建的出色 App 在运行 Apple 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)
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。