在 iPad 上用 SwiftUI 组织界面
SwiftUI on iPad: Organize your interface
2022年6月6日
一句话判断
iPadOS 16 终于把 Table、多选上下文菜单和 NavigationSplitView 三件套补齐了——用 SwiftUI 构建 iPad 生产级应用的最后几块拼图到位了。
这场 Session 讲了什么
Session 围绕三个主题展开:列表与表格、选择模型、分栏视图。
首先是 Table。SwiftUI 在 macOS Monterey 上引入的多列表格 API,现在也登陆 iPadOS 了。Table 支持多列、排序、分区,在 compact 尺寸下自动退化为单列显示。Session 用一个”安静阅读场所”App 做示例,从 List 迁移到 Table,逐步加入列定义、固定宽度、排序比较器,展示了一个信息密度更高的 iPad 界面。
然后是选择模型。iPadOS 16 引入了轻量级多选——连接键盘后不用进入编辑模式就能多选,支持 Shift 和 Command 快捷键。SwiftUI 还新增了多选上下文菜单,能根据选中的项目集合(空、单个、多个)展示不同的菜单项。Session 详细解释了 tag 和 selection state 的关系:ForEach 会自动从 explicit identity 派生 tag,Table 则用 row value 的 identifier 作为选择 tag。
最后是 NavigationSplitView。两栏和三栏布局都有详细的展示,包括横竖屏下的不同表现、automatic/balanced/prominentDetail 三种风格的区别。三栏布局在横屏下默认显示 content 和 detail 列,sidebar 通过按钮切换;竖屏下只显示 detail,依次展开。
值得深挖的点
Table 在 compact 尺寸下的优雅降级。 Table 在 slide over 或 iPhone 上只显示第一列,看起来跟 List 一模一样。但这不是简单的替换——SwiftUI 保持了 scroll position 和 selection state 的连续性,所以在尺寸变化时不会丢失用户状态。这意味着你可以放心地用 Table 替代 List,不用担心 compact 场景下的体验退化。关键原则是:第一列永远要放最重要的内容。
多选上下文菜单的设计哲学。 新的 contextMenu(selection:) 修饰符接收一个 Set<SelectionValue>,让你根据集合的元素数量决定菜单内容。空集合 = 空白区域的菜单(比如”添加新项目”),单个元素 = 单项操作,多个元素 = 批量操作。这个 API 设计很聪明——它把三种场景统一到一个闭包里,避免了分散的上下文菜单注册。注意如果空集合的闭包没有生成任何视图,SwiftUI 就不会在空白区域显示菜单。
NavigationSplitView 三栏布局的交互层级。 三栏比两栏复杂很多:sidebar -> content -> detail。竖屏下用户需要点两次按钮才能看到 sidebar,这意味着 sidebar 放的是最高层级的导航(比如分类),content 放中间层级(比如列表),detail 放最终内容。Apple 推荐 automatic 风格因为它会根据可用空间做最佳分配,别自作聪明用 balanced。
代码片段
从 List 迁移到 Table,加入排序和多列:
Table(viewModel.places, sortOrder: $sortOrder) {
// 第一列在 compact 下显示,放最重要的内容
TableColumn("Name", value: \.name) { place in
PlaceCell(place: place) // 可以复用 List 里的 cell
}
// 纯文本列可以用便捷构造器,省略 view builder
TableColumn("Comfort", value: \.comfortLevel)
.width(100) // 固定宽度,适合短内容
TableColumn("Noise", value: \.noiseLevel)
}
.onChange(of: sortOrder) { newOrder in
// Table 不自动排序,需要你自己处理数据
viewModel.places.sort(using: newOrder)
}
这段代码的关键点是 value: 参数——它同时用于排序比较和 selection tag 的自动派生。
添加多选和上下文菜单:
@State private var selection: Set<Place.ID> = []
Table(viewModel.places, selection: $selection) { /* 列定义 */ }
.contextMenu(forSelectionType: Place.ID.self) { items in
if items.isEmpty {
// 空白区域的长按菜单
Button("Add New Place") { viewModel.addPlace() }
} else if items.count == 1 {
// 单项菜单
Button("Favorite") { /* ... */ }
}
// 单选和多选都显示的操作
Button("Add to Guide") { viewModel.addToGuide(items) }
}
forSelectionType 必须和 Table 的 selection 类型匹配。Table 会自动用 row 的 identifier 作为 tag,所以你不需要手动 .tag()。
NavigationSplitView 两栏布局:
NavigationSplitView {
// sidebar 列
PlaceList(places: viewModel.places, selection: $selectedPlace)
} detail: {
if let place = selectedPlace {
PlaceDetail(place: place)
} else {
Text("Select a place") // placeholder
}
}
// automatic 风格:横屏并排,竖屏隐藏 sidebar
最佳实践
第一列要能独立承载全部信息。Table 在 compact 下只显示第一列,所以别把关键数据放在后面的列里。
测试 slide over 场景。iPad App 必须在各种窗口尺寸下都能用,Table 的自动退化帮你处理了大部分问题,但自定义布局需要你自己验证。
用 editButton 配合轻量级多选。虽然连接键盘后不需要编辑模式就能多选,但纯触控场景下用户还是需要进入编辑模式的途径。在 toolbar 里放一个 EditButton 是最简单的方案。
传递给 openWindow 的 value 应该是 identifier 而不是整个模型对象。值类型会被复制,导致两个窗口各自编辑不同的副本。用 identifier 让 model store 成为单一数据源。
还有什么值得关注
- Session 是两集系列的第一集,第二集 “SwiftUI on iPad: Add toolbars, titles, and more” 覆盖了 toolbar 和导航栏的增强
- Table 支持 sections(通过
Section嵌套在 Table 里),这在 macOS 和 iPad 上都可用 - NavigationSplitView 在 macOS 上的行为和 iPad 类似,为将来移植到 Mac 做了铺垫
- SwiftUI cookbook for navigation session 提供了更完整的导航方案,包括 state restoration 和 deep linking