SwiftUI API 设计之道:渐进式披露的四个策略
The craft of SwiftUI API design: Progressive disclosure
2022年6月6日
一句话判断
这不是一个教你用 SwiftUI 的 session,而是教你设计 SwiftUI API 的 session——Apple 首次公开了他们设计 API 时的核心原则「渐进式披露」,并且用大量真实代码演化过程来论证。
这场 Session 讲了什么
SwiftUI 团队的 Sam Lazarus 从一个很巧妙的角度切入:macOS 的保存对话框。简单模式只显示文件名和几个常用位置,展开后变成完整的文件浏览器。不同复杂度的需求看到不同复杂度的 UI——这就是渐进式披露(Progressive Disclosure)。
把这个概念搬到 API 设计上:调用者的复杂度应该和使用场景的复杂度成正比。一个理想的 API 对简单场景极简,对复杂场景足够强大。
Session 给出了四个策略。第一,考虑常见用例——Button 的 label 99% 的情况下是纯文本,所以 Button("Next Page") { } 是最常见的调用方式。第二,提供智能默认值——Text("Hello") 背后自动做了本地化、Dark Mode 适配、Dynamic Type 缩放、行间距调整,你不需要指定任何一项。第三,优化调用点——Table 从完全体到最简体经过四步精简,最终每个字符都有明确用途。第四,组合而非枚举——HStack 的间距不用 enum 来枚举所有排列方式,而是用 Spacer 这个可组合的元素来表达无限种间距方案。
值得深挖的点
「从调用者角度设计」是一个被低估的原则。 大多数开发者在写可复用组件时,习惯从声明端(declaration site)出发——怎么定义结构体、怎么组织内部逻辑。但 API 的质量应该从调用端(call site)来衡量。Session 反复强调:你的代码在被使用的地方看起来是什么样,比在你写的地方看起来是什么样重要得多。Table API 的演化过程是最好的例子——同一个功能,从几十行参数缩减到几行,但能力一点没少。
「组合而非枚举」避免了 API 的死亡螺旋。 Session 用 HStack 的间距设计来说明:如果你用 enum 来枚举 leading/centered/trailing/evenly-spaced/between-elements… 你永远枚举不完所有有用的情况。但用 Spacer 这个可组合元素,上面的所有排列以及更多都能表达。这个原则不仅适用于布局,适用于任何需要灵活性的 API 设计——用小而正交的构建块组合出复杂行为,而不是用一个越来越大的 enum 来覆盖所有 case。
代码片段
Table API 的四步精简过程:
// 第一步:完全体,所有细节都显式指定
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: {
ForEach(currentlyReading) { book in
TableRow(book)
}
}
.onChange(of: sortOrder) { newValue in
currentlyReading.sort(using: newValue)
}
// 第二步:集合直接传入 Table,省掉 ForEach
Table(currentlyReading, sortOrder: $sortOrder) { ... }
// 第三步:String keypath 省掉闭包
TableColumn("Title", value: \.title) // 不需要 { Text($0.title) }
// 最终:最简形态,不需要排序的就省掉 sortOrder
Table(currentlyReading) {
TableColumn("Title", value: \.title)
TableColumn("Author", value: \.author)
}
组合 vs 枚举的对比:
// 枚举方式(不推荐):永远枚举不完
enum Arrangement { case leading, centered, trailing, evenlySpaced, ... }
// 组合方式(推荐):用 Spacer 表达无限种间距方案
HStack { Box(); Spacer(); Box(); Box() } // space before last
HStack { Spacer(); Box(); Spacer(); Box(); Spacer() } // centered
HStack { Box(); Spacer(); Box(); Spacer(); Box(); Spacer() } // evenly spaced
// 任何你想要的间距方案,都能用 Spacer 组合出来
智能默认值的威力——Text 做了什么:
// 你写的
Text("Hello WWDC22!")
// SwiftUI 自动处理的:
// - 从 app bundle 查找本地化字符串
// - 适配当前 color scheme(Dark Mode)
// - 根据 Dynamic Type 缩放
// - 多个 Text 相邻时自动调整行间距
// 你不需要指定以上任何一项
最佳实践
- 设计可复用组件时,先写两三处实际调用点,从调用端反推声明端的最佳形态。
- 为 99% 的常见用例提供便捷入口,1% 的用例用完整版 API。
- 在每次添加参数前问自己:这真的是每次调用都需要指定的吗?如果不是,把它变成默认值。
- 遇到需要枚举很多 case 的场景,停下来想想能不能拆成可组合的小单元。
- 用
some返回类型隐藏实现细节,让调用者不依赖你的内部类型。
还有什么值得关注
- Session 提到的 macOS save dialog 是渐进式披露的经典 UI 实例,可以用来说服团队接受这个设计理念。
- Toolbar items 不指定 placement 时按平台惯例自动放置(macOS leading,iOS trailing,watchOS 只显示第一个)。
- SwiftUI 内部的 Apple 团队自己用这些原则在重写和优化每一个新 API。