大多数浏览器和
Developer App 均支持流媒体播放。
-
解密 SwiftUI 容器
了解 SwiftUI 容器视图的众多功能,并针对容器如何管理相应的子视图建立思维模型。利用新的 API 来构建专属的自定容器、创建修饰符来自定容器内容,并进一步润色你的容器,帮助你的 App 脱颖而出。
章节
- 0:00 - Introduction
- 3:17 - Composition
- 10:42 - Sections
- 13:18 - Customization
- 16:52 - Next steps
资源
相关视频
WWDC23
WWDC21
-
下载
大家好 我叫 Matt 就职于 SwiftUI 团队 这个视频主要介绍如何 在 SwiftUI 中构建自定容器视图 SwiftUI 在它的 API 中提供 很多种功能全面的容器 如 List 容器视图
容器视图使用后置视图构建器闭包 来封装内容
视图构建器允许静态定义内容 比如这个硬编码的 Text 视图列表 但也可以动态定义内容 例如使用 ForEach 视图 基于数据生成 Text 视图
视图构建器支持在同一个容器中 混合编排各种类型的内容 有的容器还支持更高级的功能 比如将内容分为不同的组 具有可配置的标题和页脚 并可添加特定于容器的修饰符 用于自定内容 在这个示例中 我隐藏了 列表通常在行与行之间 绘制的分隔线 在这个视频中 我将展示如何使用几个新的 API 构建自定容器视图 并支持 所有这些功能以及更多功能
我将首先说明如何使自定容器 支持任意内容组合 最大限度地提高灵活性
我还将演示如何添加分组支持
然后 我将介绍 如何定义自定修饰符 来修饰容器中的内容
对于每个主题 我将讨论 SwiftUI 和 API 设计背后的 一些核心概念 我们这就开始 首先
这是什么?
哇 “SwiftUI 的新功能”讲座 的 Sommer 和 Sam 要举办卡拉 OK 派对 来庆祝 WWDC
我需要回复我打算在派对上唱的歌
但我不知道该选哪首歌 在这样的危机时刻 我会一如既往 利用 SwiftUI 声明式 API 的强大功能和灵活性 来解决问题 在这个视频中 我还将挑选 完美的卡拉 OK 歌曲 我制作了一个好用的工具 这就是我的展示板
我已经开始集思广益 挑选了几首备选歌曲
我使用一个构造器 将我挑选的歌曲 映射到 Text 视图中 把歌名写在卡片上 并将卡片钉在展示板上
在 DisplayBoard 的实现中 我使用自定布局 使卡片钉在展示板上的随机位置 卡片本身使用 ForEach 视图来构造 它负责处理输入数据的遍历 根据每个数据元素生成内容视图 并封装在我创建的 自定 CardView 中
这是个好的开始 但 DisplayBoard 容器 限制了我的创造力 它只允许根据单一数据类型 构造卡片 我可以让容器变得更灵活 只需要添加 对更多种类的内容组合的支持
但首先需要了解组合的含义
例如有这样一个 SwiftUI 列表 显示了 Sam 向我推荐的一些歌曲
这个列表使用一组数据进行初始化 就像我的 DisplayBoard 一样 但 SwiftUI 还支持 以其他方式创建列表
例如 我可以通过手动写出 一组视图来创建列表 这就是我创建我自己的 备选歌曲列表的方式
SwiftUI 融合了这两种方法 它提供各种 API 可将不同种类的内容组合起来
例如 我可以使用 ForEach 视图 重写基于数据的列表 这支持与之前相同的功能 但 ForEach 视图 可以嵌套在视图构建器中
这很重要 因为只需要使用视图 就可以定义两个列表的内容
这意味着我可以将它们 整合到一个统一的列表中 显示我到目前为止收集的所有歌曲 这个统一的列表 就是组合的一个例子
我可以使用 硬编码的 Text 视图 静态定义前三行 同时使用数据动态生成其余的行 而所有这些行都在同一个列表中
我还想在 DisplayBoard 容器中 支持灵活的组合 为此 我需要更改我的实现
第一步是 重构我的容器 使它只需要使用视图构建器 就可以进行初始化
首先 我将现有的基于数据的属性 替换为一个通用的视图属性
通过添加 ViewBuilder 属性 我的默认构造器 会使用后置视图构建器闭包 自动构造内容
接下来 我需要更新视图正文 以便使用新的内容视图 为此 我可以使用一个新的 API 称为 ForEach(subviewOf:)
这个新的 ForEach 构造器 接受单个视图值作为输入 并将它的每个子视图传回 后置视图构建器中 使它们转变为不同类型的视图 比如我的卡片视图
通过这个新的实现 现在我可以获取 之前的备选歌曲列表
并将相同的内容加入到 DisplayBoard 中 将每个 Text 视图 转换为展示板上的一个卡片
这是一个很大的进步 但我们需要了解它的工作原理
回到我的实现 我将深入研究新的 API
ForEach(subviewOf:
什么是子视图?
子视图 是包含在另一个视图中的视图 仔细看内容 里面有多少个子视图? 答案是 这要看情况
如果只考虑代码中的顶层视图 那么有四个视图 三个 Text 视图 和一个 ForEach 视图
但是 ForEach 不是只有一个视图 它表示根据数据生成的一系列视图
在这个例子中 它解析为九个子视图 每个子视图对应 Sam 喜欢的一首歌
因此 这个 DisplayBoard 的内容 实际上解析为 总共十二个不同的子视图 显而易见 展示板上显示了十二张卡片
这与列表中的内容也是一致的 也就是十二个不同的行
我们需要了解 这两种子视图之间的区别
DisplayBoard 代码中的 四个子视图 以橙色高亮显示 称为声明子视图
而将显示在屏幕上的视图 以蓝色高亮显示 称为解析子视图 这包括我手动定义的 三个 Text 视图 以及 ForEach 生成的 九个 Text 视图
在 SwiftUI 的声明式系统中 声明子视图定义食谱 用于在 SwiftUI App 运行时 生成解析子视图
例如 ForEach 视图是声明子视图 本身并没有具体的视觉外观或行为 ForEach 视图的全部目的在于 生成一组解析子视图
Group 视图 是内置容器的另一个例子 它代表一组解析子视图 例如 三个 Text 视图的 Group 将解析为三个对应的子视图
有些声明子视图甚至可以 不生成任何解析子视图 比如 EmptyView
或者会有条件地解析为 其他数量的子视图 比如 if 语句的不同分支
新的 ForEach(subviewOf:) API 仅遍历解析子视图的内容
这使容器能够支持 任何内容组合 并且需要的代码更少 因为 SwiftUI 将替我 解析子视图 无论这些子视图在代码中如何声明
由于支持灵活的组合 将新歌曲添加到展示板 变得非常简单
除了 Sam 的歌曲 Sommer 也非常贴心地 推荐了一些她喜欢的歌曲 我可以使用另一个 ForEach 视图 来添加这些歌曲 而不需要 对容器的实现进行任何额外更改 但是 由于轻易就可以 添加很多新想法 查看所有的卡片变得有点费劲 为了解决这个问题 当展示板变得太拥挤时 我将缩减卡片的大小
我希望当展示板上添加了 超过 15 张卡片的时候 缩减卡片的大小 为了计算卡片的数量 我可以使用另一个新的 API 称为 Group(subviewsOf:) 我可以将它包围在 实现中的 ForEach 外面
跟前面的 ForEach(subviewOf:) API 一样 这个视图接受视图作为输入 并解析它的子视图
但不是对子视图逐个进行遍历 Group(subviewsOf:) API 会传回 所有解析子视图的集合
我可以对集合使用 count 属性 来查看卡片的总数 配置 CardView 当有超过 15 张卡片时 使用更小的大小
当我重新运行 App 时 较小的尺寸避免了卡片有过多重叠 卡片变得更容易阅读 但我的展示板看起来还是有点杂乱
接下来我会对卡片进行整理 为此我将添加分组支持
列表就是一个例子 它是支持分组的 内置容器 它使用 SwiftUI 的 Section 视图 Section 视图的行为 与 Group 视图很相似 但它具有特定于分组的额外元数据 如可选的标题和页脚
对于我的展示板 我的目标是为每个人喜欢的歌曲 创建一个单独的分组
但是自定容器 在默认情况下不支持分组 所以我需要做一些额外的工作 来使它支持分组
这是我所构思的设计的草图 将展示板划分成几个垂直的列 顶部显示标题
在我的实现中 首先我将把现有的卡片布局代码 重构到它自己的视图中
在各个组中安排卡片布局时 我将重用这个视图
接下来 我会将组内容 封装到一个水平堆栈中 用于将展示板分成几列 为了构建列 我需要访问 展示板的内容中存在的 任何 Section 视图的信息 为此 我将对 ForEach 使用 另一个新的 API
称为 ForEach(sectionOf:) 它的工作方式与 ForEach(subviewOf:)类似 接受视图实例作为输入
但这个版本会遍历 它在视图中检测到的每个分组 将组配置添加到视图构建器中
每个组都有一个用于 内容视图的属性 我可以将这个属性传递给 之前创建的帮助器视图 用于设置卡片布局
为了进一步优化 我将为每个组添加自定背景 这有助于更直观地区分各个组
再次运行 App 可以看到卡片排列比之前更有条理 每个组各占一列 现在我将添加对显示组标题的支持
首先我在每个组的外层 添加一个 VStack 来保存标题和内容
接下来 我将使用 if 语句 和 isEmpty 属性 来检查这个组是否有标题 它会返回标题是否包含 任何解析子视图
如果标题存在 我会将它显示在 我之前编写的自定标题卡片中
查看展示板 现在每个组上方 多了一个清晰可辨的标题卡片
但为了挑选歌曲 我需要开始划掉一些备选项 为此我可以添加支持 用于自定义容器的内容
在视频一开始 我展示了一个使用 .listRowSeparator() 修饰符的例子 尽管这个修饰符 应用于 List 中的视图 但是当决定在行与行之间 绘制分隔线时 List 本身负责实现这个行为
在我的展示板中 我想支持修改卡片 如果我决定不选择某一首歌曲 我可以把它划掉
我可以使用一个新的 API 来构建 这种特定于容器的修饰符 称为容器值 容器值是一种新型键控存储 类似于环境和偏好等概念
但环境值 沿着整个视图层次结构向下传递
偏好值沿着整个视图层次结构 向上传递 到每个包含视图
解析子视图的容器值 只能被自己的直接容器访问 这使它非常适合用于 实现特定于容器的定制选项
在我的展示板中 我要使用容器值 创建自定视图修饰符 用于划掉卡片 定义一类新的容器值 只需要几行代码
首先 我将创建 ContainerValues 类型的扩展 这是 SwiftUI 中的新类型
在我的扩展中 我将使用新的 Entry 宏 声明一个属性 用于存储一个布尔值 来跟踪卡片是否被拒绝
Entry 宏是一个新的 API 它提供了一种便捷的语法 用于向 SwiftUI 键控存储类型 添加新值 包括环境价值、关注值等等
然后 我将声明一个自定视图修饰符 来方便地设置我的属性 它调用新的 containerValue() API 修饰符 传递属性的键路径 以及要设置的新值
现在 我要在容器中添加 对新容器值的支持 在分组实现中 我需要自定义每个卡片视图 取决于卡片内容是否被拒绝 为此我可以使用 新的 containerValues 属性 容器值可以从 解析子视图和分组读取
我会将自定值传递给 CardView 中的 isRejected 参数 当卡片被拒绝时 将显示一个自定声明
利用新的修饰符 我现在可以开始删除一些歌曲了
首先 我很喜欢 Scrolling in the Deep 这首歌 但我不确定自己的音域 是否能够胜任
所以我会在展示板上划掉它 用红色的粗斜杠表示
Sam 已经抢先预定了几首歌 所以我也会划掉这几首歌
我不确定 Sommer 打算唱什么歌 为了保险起见 我将划掉她推荐的所有歌曲
将修饰符应用于整个组 会在它的所有子视图上设置值
也就是说 右侧 Sommer 的所有歌曲 都被划掉了
好了 我已经取得了很大的进展 很快就要找到完美的卡拉 OK 歌曲 但我还没有最后决定 趁着我还在思考 我鼓励大家尝试这些新的 API
在 ForEach 和 Group 上 使用新的构造器 遍历并转换 解析子视图和视图的各个组 添加分组支持 如果你的自定容器设计可以支持 但如果分组在你的容器中没有意义 也没关系 添加分组支持并不是强制的
最后 使用容器值自定义和装饰 各部分内容
在这些新 API 的帮助下 我已经缩小了选择范围 现在只剩下几首歌了
但我突然想到 还有一首歌 我之前没有考虑过 我认为它可能是最合适的歌
那就是 Whitney View-ston 的 “I Will Always Subview”
现在还剩下一件事要做 回复 Sommer 和 Sam 并完成这个视频 因为我真的要开始排练我的歌了! 再会!
-
-
0:20 - SwiftUI Lists
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") }
-
0:36 - SwiftUI Lists
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(otherSongs) { song in Text(song.title) } }
-
0:54 - SwiftUI Lists
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) } } }
-
1:00 - SwiftUI Lists
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) .listRowSeparator(.hidden) } } }
-
2:35 - Data-driven DisplayBoard
@State private var songs: [Song] = [ Song("Scrolling in the Deep"), Song("Born to Build & Run"), Song("Some Body Like View"), ] var body: some View { DisplayBoard(songs) { song in Text(song.title) } }
-
2:47 - DisplayBoard implementation
// Insert code snvar data: Data @ViewBuilder var content: (Data.Element) -> Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } }
-
3:08 - Data-driven DisplayBoard
@State private var songs: [Song] = [ Song("Scrolling in the Deep"), Song("Born to Build & Run"), Song("Some Body Like View"), ] var body: some View { DisplayBoard(songs) { song in Text(song.title) } }
-
3:30 - List composition
List(songsFromSam) { song in Text(song.title) }
-
3:46 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") }
-
3:56 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } List(songsFromSam) { song in Text(song.title) }
-
4:05 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } List { ForEach(songsFromSam) { song in Text(song.title) } }
-
4:24 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
4:59 - DisplayBoard implementation
var data: Data @ViewBuilder var content: (Data.Element) -> Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } }
-
5:15 - DisplayBoard implementation
// DisplayBoard implementation @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } } DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } DisplayBoard { ForEach(songsFromSam) { song in Text(song.title) } }
-
5:27 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
5:52 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
5:57 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
6:12 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
6:23 - DisplayBoard subviews
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
6:36 - Declared vs. resolved views
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
7:11 - List subviews
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
7:19 - Declared vs. resolved views
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
8:00 - Resolved ForEach
// 1 declared view ForEach(songsFromSam) { song in Text(song.title) } // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
8:16 - Resolved Group
// 1 declared view Group { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View")
-
8:32 - Resolved EmptyView
// 1 declared view EmptyView() // Zero resolved subviews
-
8:39 - Resolved if expression
// Insert code snippet.
-
8:48 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
9:11 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
9:17 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } ForEach(songsFromSommer) { song in Text(song.title) } }
-
9:44 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
9:55 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView { subview } } } } .background { BoardBackgroundView() } }
-
10:19 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView( scale: subviews.count > 15 ? .small : .normal ) { subview } } } } .background { BoardBackgroundView() } }
-
10:47 - List sections
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) } } }
-
11:03 - DisplayBoard sections
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) } } Section("Sommer's Favorites") { ForEach(songsFromSommer) { song in Text(song.title) } } }
-
11:26 - Implementing DisplayBoard sections
DisplayBoard sections @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView( scale: subviews.count > 15 ? .small : .normal ) { subview } } } } .background { BoardBackgroundView() } }
-
11:35 - Implementing DisplayBoard sections
@ViewBuilder var content: Content var body: some View { DisplayBoardSectionContent { content } .background { BoardBackgroundView() } } struct DisplayBoardSectionContent<Content: View>: View { @ViewBuilder var content: Content ... }
-
11:42 - Implementing DisplayBoard sections
@ViewBuilder var content: Content var body: some View { HStack(spacing: 80) { ForEach(sectionOf: content) { section in DisplayBoardSectionContent { section.content } } } .background { BoardBackgroundView() } }
-
12:48 - Implementing DisplayBoard section headers
@ViewBuilder var content: Content var body: some View { HStack(spacing: 80) { ForEach(sectionOf: content) { section in VStack(spacing: 20) { if !section.header.isEmpty { DisplayBoardSectionHeaderCard { section.header } } DisplayBoardSectionContent { section.content } .background { BoardSectionBackgroundView() } } } } .background { BoardBackgroundView() } }
-
13:30 - List customization
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) .listRowSeparator(.hidden) } } }
-
14:46 - Custom container values
extension ContainerValues { @Entry var isDisplayBoardCardRejected: Bool = false } extension View { func displayBoardCardRejected(_ isRejected: Bool) -> some View { containerValue(\.isDisplayBoardCardRejected, isRejected) } }
-
15:42 - Implementing DisplayBoard customization
struct DisplayBoardSectionContent<Content: View>: View { @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in let values = subview.containerValues CardView( scale: (subviews.count > 15) ? .small : .normal, isRejected: values.isDisplayBoardCardRejected ) { subview } } } } } }
-
16:15 - DisplayBoard customization
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") .displayBoardCardRejected(true) Text("Born to Build & Run") Text("Some Body Like View") } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) .displayBoardCardRejected(song.samHasDibs) } } Section("Sommer's Favorites") { ForEach(songsFromSommer) { Text($0.title) }}} } .displayBoardCardRejected(true) }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。