Highlight
SwiftUI 团队的 Sam Lazarus 从一个很巧妙的角度切入:macOS 的保存对话框。简单模式只显示文件名和几个常用位置,展开后变成完整的文件浏览器。不同复杂度的需求看到不同复杂度的 UI——这就是渐进式披露(Progressive Disclosure)。
核心内容
这场 Session 讲 SwiftUI 团队如何设计 API。
Sam Lazarus 给出的定义很直接:渐进式披露(Progressive Disclosure)就是让调用点(call site)的复杂度,随着使用场景的复杂度增长(02:19)。简单场景只写必要信息。复杂场景再打开更多能力。
这和 macOS 的保存对话框类似。默认状态已经给出保存位置和常用位置;需要浏览文件系统时,用户再展开完整界面(01:14)。SwiftUI 想把这种体验放进 API:初次使用快,后续扩展不受限。
Sam 强调,任何可复用组件或抽象都是 API。写组件的人通常看声明处(declaration site),但使用体验发生在调用处(call site)(02:00)。这也是本场的主线:从调用处反推 API 形状。
详细内容
从常见用例开始
(04:18)SwiftUI 的 Button 是第一个例子。按钮必须有 label。大多数按钮的 label 只是文字,所以最常见的写法很短。
Button("Next Page") {
currentPage += 1
}
关键点:
Button("Next Page")把最常见的文字 label 放到第一层 API。- 闭包里只保留按钮动作,这里是把当前页加一。
- 调用点没有出现
Text、HStack或额外参数,因为这个场景不需要它们。
当 label 需要自定义时,SwiftUI 再提供完整版本(04:36)。
Button {
currentPage += 1
} label: {
Text("Next Page")
}
关键点:
- 第一个闭包仍然是按钮动作。
label闭包开始暴露 label 的视图结构。Text("Next Page")让调用者可以继续套用普通 SwiftUI 视图能力。
更复杂的 label 继续沿用同一个入口(04:43)。
Button {
currentPage += 1
} label: {
HStack {
Text("Next Page")
NextPagePreview()
}
}
关键点:
HStack把文字和预览视图组合成一个 label。Text("Next Page")仍然承担可读名称。NextPagePreview()是额外的自定义内容,只在需要时出现。
这就是渐进式披露的第一条策略:先识别最常见的用例,再为它提供简洁入口。复杂能力保留在下一层 API 里。
给常见场景设置智能默认值
(05:30)Text 是智能默认值的例子。
Text("Hello WWDC22!")
关键点:
- 调用点只写出要显示的内容。
- SwiftUI 会用环境里的 locale 到 app bundle 中查找本地化字符串。
- 文本会自动适配当前颜色方案,支持深色模式。
- 文本会根据当前辅助功能 Dynamic Type 尺寸自动缩放。
这些行为都可以手动指定。但在普通场景里,它们不该占据调用点。
Text 还有一个细节。两个 Text 放进栈里时,文本之间的间距会自动调整为当前上下文的正确行距(06:12)。
VStack {
Text("Hello WWDC22!")
Text("Call to Code.")
}
关键点:
VStack只表达两个文本的垂直排列。- 每个
Text仍然只声明内容。 - 行距这类排版细节由 SwiftUI 默认值处理。
Toolbar 也使用同一思路。没有指定位置时,SwiftUI 会按平台惯例放置按钮:macOS 放在 toolbar leading edge,iOS 放在 navigation bar trailing edge,watchOS 只显示第一个 item 并固定在 navigation bar 下方(06:43)。需要控制位置时,再使用带 placement 的 ToolbarItemGroup(07:20)。
优化调用点:Table 的四次收缩
(08:09)Table 是本场最完整的例子。多列表格可以支持排序、富内容单元格、分组 rows 等能力。完整写法很强,但普通表格不该每次都写完整结构。
复杂版本长这样:
@State var sortOrder = [KeyPathComparator(\Book.title)]
var body: some View {
Table(sortOrder: $sortOrder) {
TableColumn("Title", value: \Book.title) { book in
Text(book.title).bold()
}
TableColumn("Author", value: \Book.author) { book in
Text(book.author).italic()
}
} rows: {
Section("Favorites") {
ForEach(favorites) { book in
TableRow(book)
}
}
Section("Currently Reading") {
ForEach(currentlyReading) { book in
TableRow(book)
}
}
}
.onChange(of: sortOrder) { newValue in
favorites.sort(using: newValue)
currentlyReading.sort(using: newValue)
}
}
关键点:
sortOrder保存当前排序规则。Table(sortOrder:)允许表格根据列标题点击改变排序状态。TableColumn声明列名、取值 key path 和单元格视图。Section把 rows 分成不同区域。ForEach把集合中的每本书转换成TableRow。.onChange(of: sortOrder)在排序变化时重新排序数据源。
这个版本适合复杂表格。接下来 SwiftUI 逐步移除普通场景不需要的信息。
第一步,普通 rows 经常只是对集合做 ForEach,再为每个元素生成 TableRow。SwiftUI 让集合直接传给 Table(09:58)。
@State var sortOrder = [KeyPathComparator(\Book.title)]
var body: some View {
Table(currentlyReading, sortOrder: $sortOrder) {
TableColumn("Title", value: \.title) { book in
Text(book.title)
}
TableColumn("Author", value: \.author) { book in
Text(book.author)
}
}
.onChange(of: sortOrder) { newValue in
currentlyReading.sort(using: newValue)
}
}
关键点:
Table(currentlyReading, sortOrder:)把数据集合交给表格。- 调用点不再手写
ForEach和TableRow。 \.title、\.author使用相对 key path,因为集合元素类型已经由currentlyReading推出。- 排序逻辑仍然保留,因为这个版本还支持排序。
第二步,如果列的 value key path 指向字符串,最常见的显示方式就是 Text。SwiftUI 允许省略单元格视图(10:23)。
@State var sortOrder = [KeyPathComparator(\Book.title)]
var body: some View {
Table(currentlyReading, sortOrder: $sortOrder) {
TableColumn("Title", value: \.title)
TableColumn("Author", value: \.author)
}
.onChange(of: sortOrder) { newValue in
currentlyReading.sort(using: newValue)
}
}
关键点:
TableColumn("Title", value: \.title)已经足够生成标题列。TableColumn("Author", value: \.author)同理生成作者列。- 单元格中的
Text(book.title)和Text(book.author)被默认行为覆盖。 - 排序绑定和排序回调仍然显示在调用点。
第三步,最简单的表格不关心排序。SwiftUI 再提供不带 sort order 的版本(10:51)。
var body: some View {
Table(currentlyReading) {
TableColumn("Title", value: \.title)
TableColumn("Author", value: \.author)
}
}
关键点:
Table(currentlyReading)表示数据源。- 两个
TableColumn表示必须显示的列。 - 没有
sortOrder,因为这个用例不需要排序。 - 没有 rows 闭包和 cell 闭包,因为默认行为足够表达这个表格。
Sam 总结了两个检查问题:哪些常见用例值得做 convenience;哪些信息属于永远必须提供的 essential information(11:03)。Table 的最终形态正是这两个问题的结果。
用组合替代枚举所有可能
(11:37)最后一个策略是 Compose, don’t enumerate。
HStack 的核心信息有两类:内容是什么,以及这些内容如何排列。内容已经由 view builder 表达。排列方式如果用 enum 设计,很快会失控:leading、center、trailing 之后,还会有均匀分布、只在元素之间留空、只在最后一个元素前留空等情况(12:08)。
SwiftUI 没有为每种排列增加 enum case,而是提供 Spacer,让调用者组合出排列。
struct StackExample: View {
var body: some View {
HStack { // centered
Spacer()
Box().tint(.red)
Box().tint(.green)
Box().tint(.blue)
Spacer()
}
}
}
关键点:
HStack仍然只负责水平排列内容。- 第一个
Spacer()把盒子整体向中间推。 - 三个
Box().tint(...)是实际内容。 - 最后的
Spacer()与第一个 spacer 配合,形成居中效果。
均匀分布也不用新增 API(13:42)。
struct StackExample: View {
var body: some View {
HStack { // evenly spaced
Spacer()
Box().tint(.red)
Spacer()
Box().tint(.green)
Spacer()
Box().tint(.blue)
Spacer()
}
}
}
关键点:
- 每个
Spacer()都是一个可组合的布局元素。 - 内容之间插入 spacer 后,空白被分摊到多个位置。
- API 没有增长,表达能力却扩展了。
只在元素之间留空也一样(13:43)。
struct StackExample: View {
var body: some View {
HStack { // space only between elements
Box().tint(.red)
Spacer()
Box().tint(.green)
Spacer()
Box().tint(.blue)
}
}
}
关键点:
Box().tint(.red)、Box().tint(.green)、Box().tint(.blue)是三个内容视图。- 两个
Spacer()只出现在内容之间。 - 开头和结尾没有 spacer,所以空白不会被放到两端。
这部分提醒很实用:不要把“常见情况”误解成“把所有常见情况都写成枚举”。如果可能性会继续增长,就应该拆成可组合的小单元(13:20)。
核心启发
-
做什么:为团队内部的 SwiftUI 组件写一个最短调用入口。 为什么值得做:Session 反复强调调用点复杂度要跟使用场景匹配。常见场景应该先跑起来。 怎么开始:找出组件的 80% 调用方式,为它添加一个只包含必要参数的 initializer,再把高级配置留给完整 initializer。
-
做什么:给自定义 view 的参数设置智能默认值。 为什么值得做:
Text和 toolbar 的例子说明,默认值可以把本地化、颜色方案、平台惯例这类细节从普通调用点移走。 怎么开始:检查每个参数是否每次都必须写;可省略的参数,改成默认参数或从Environment中读取。 -
做什么:重写一个表格或列表组件的调用点。 为什么值得做:Table 的例子展示了从完整能力到简单入口的收缩过程。 怎么开始:先保留完整版本,再为集合输入、字符串列、无排序场景分别添加 convenience API。
-
做什么:把 enum 配置拆成可组合的 view 或 modifier。 为什么值得做:HStack 没有枚举所有排列方式,而是用
Spacer让开发者组合布局。 怎么开始:如果某个 enum 不断增加 case,先问这些 case 能不能由更小的结构组合出来。 -
做什么:在写 API 前先写调用示例。 为什么值得做:本场的核心视角是 call site,优先从使用处检查 API 是否顺手。 怎么开始:先写三段目标调用代码:最常见、稍复杂、最高级。再根据这些调用点反推类型、initializer 和默认值。
关联 Session
- The SwiftUI cookbook for navigation — 数据驱动导航 API 展示了 SwiftUI 如何把复杂导航结构拆成可逐步采用的接口。
- Compose custom layouts with SwiftUI — 自定义布局延续了本场的组合思路,用协议和布局容器表达更复杂的界面结构。
- SwiftUI on iPad: Organize your interface — 这场使用列表、表格和分栏视图解释 SwiftUI 如何组织桌面级 iPad 界面。
- Bring multiple windows to your SwiftUI app — 多窗口 API 展示 SwiftUI scene 设计如何兼顾简单入口和高级窗口管理。
评论
GitHub Issues · utterances