Highlight
Session 围绕三个主题展开:列表与表格、选择模型、分栏视图。
核心内容
一个 iPhone 版列表搬到 iPad 上,最常见的问题是空。屏幕变大了,信息密度没有变。用户仍然只能看到一列内容,还要不断点进点出。
这场 session 用一个“安静阅读场所”App 做例子。原来的界面只是 List,在 iPad 上浪费空间。iPadOS 16 让 SwiftUI 的多列表格(Table)登陆 iPad,同一套 API 也适用于 macOS。列表可以改成表格,名称、舒适度、噪音等级分别放进列里,用户能直接排序。
表格只是第一步。生产力 App 还需要选择、批量操作、上下文菜单。SwiftUI 的选择模型用 tag 和 selection state 配合,Table 会自动用 row value 的 identifier 做选择 tag。iPadOS 16 还加入轻量级多选:接上键盘后,用户不用进入编辑模式,也能用 Shift 和 Command 扩展选择。
最后是结构。很多 iPad App 仍然用弹窗和层层跳转承载主要流程。SwiftUI 在 iPadOS 16 和 macOS Ventura 增强了 NavigationSplitView,支持两栏、三栏和不同 split view style。它让 sidebar、content、detail 在横屏、竖屏、Slide Over、compact size class 下自动调整,减少模态界面的使用。
详细内容
从 List 迁移到 Table
(03:10)示例从一个普通列表开始。它能展示数据,但只使用单列。
struct PlacesList: View {
@Binding var modelData: ModelData
var body: some View {
List(modelData.places) { place in
PlaceCell(place)
}
}
}
关键点:
@Binding var modelData:列表直接使用外部传入的数据源。List(modelData.places):每个Place生成一行。PlaceCell(place):原有的 cell 视图可以继续复用。
(03:18)Table 的构造方式不同。它接收的是 column builder。第一列仍然可以复用 PlaceCell,后面的列用 TableColumn 展示其它字段。
struct PlacesTable: View {
@Binding var modelData: ModelData
@State private var sortOrder = [KeyPathComparator(\Place.name)]
var body: some View {
Table(modelData.places, sortOrder: $sortOrder) {
TableColumn("Name", value: \.name) { place in
PlaceCell(place)
}
TableColumn("Comfort Level", value: \.comfortDescription).width(200)
TableColumn("Noise", value: \.noiseLevel) { place in
NoiseLevelView(level: place.noiseLevel)
}
}
.onChange(of: sortOrder) {
modelData.sort(using: $0)
}
}
}
关键点:
@State private var sortOrder:保存当前排序比较器,初始按名称排序。Table(modelData.places, sortOrder: $sortOrder):把数据和排序状态绑定到表格。TableColumn("Name", value: \.name):列名用于 header,key path 用于排序。PlaceCell(place):第一列保留紧凑布局需要的完整信息。TableColumn("Comfort Level", value: \.comfortDescription).width(200):字符串列可省略 view builder,并设置固定宽度。TableColumn("Noise", value: \.noiseLevel):非纯文本内容仍然用 view builder。.onChange(of: sortOrder):Table 不会替你重排数据,数据源排序要自己执行。
这里有两个 iPad 特有的限制。第一,compact size class 下 Table 只显示第一列,所以第一列要能独立表达行内容。第二,iPad 上的 Table 不横向滚动,列数要控制,否则信息会挤在一起。
选择模型:tag 和 selection state
(06:22)SwiftUI 的选择模型由两部分组成:每一行的 tag,以及保存选中项的 state。多选时,state 通常是一个 Set,里面存放被选中行的 tag。
(07:45)tag 和 identity 接近,但不相同。ForEach 会从 explicit identity 自动派生 tag。Table 会用 row value 的 identifier 作为选择 tag。手动设置 tag 时,用 tag modifier,值必须是 Hashable,同一个可选择容器里的 tag 类型要一致。
(10:25)给 Table 加多选,只需要增加 selection state,并把 binding 传给 Table。
struct PlacesTable: View {
@EnvironmentObject var modelData: ModelData
@State private var sortOrder = [KeyPathComparator(\Place.name)]
@State private var selection: Set<Place.ID> = []
var body: some View {
Table(modelData.places, selection: $selection, sortOrder: $sortOrder) {
// columns
}
}
}
关键点:
@EnvironmentObject var modelData:表格从环境读取共享模型。@State private var sortOrder:排序状态仍然保留。@State private var selection: Set<Place.ID>:多选状态用Set保存多个行 identifier。selection: $selection:Table 通过 binding 读写当前选择。Set<Place.ID>:类型要和 Table 行 identifier 匹配。// columns:列定义可以沿用前面的 TableColumn 写法。
(09:06)iPadOS 16 新增轻量级多选。连接键盘后,用户不必进入 edit mode,就能用 Shift 和 Command 多选。触控场景仍然需要 edit mode,系统也自动支持双指 pan 进入多选手势。
用 toolbar 暴露选择操作
(10:56)选择本身没有价值,批量动作才有价值。示例在 toolbar 里加入“把选中地点加入 guide”的按钮,并保留 EditButton 供没有键盘的用户进入编辑模式。
Table(modelData.places, selection: $selection, sortOrder: $sortOrder) {
...
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
if !selection.isEmpty {
AddToGuideButton(selection)
}
}
ToolbarItemGroup(placement: .navigationBarLeading) {
EditButton()
}
}
关键点:
selection: $selection:toolbar 的显示逻辑依赖同一份选择状态。if !selection.isEmpty:没有选中项时,不显示批量操作按钮。AddToGuideButton(selection):动作接收当前选中的 identifier 集合。EditButton():没有键盘时,用户仍然有明确入口进入和退出 edit mode。
这个设计照顾两类 iPad 使用方式。键盘和触控板用户可以直接多选;纯触控用户仍然能通过 edit mode 完成同样操作。
多选上下文菜单
(11:41)SwiftUI 在 iOS 16、iPadOS 16、macOS Ventura 加入 multi-select context menus。菜单闭包接收一组 selected identifiers。集合为空,表示用户在空白区域打开菜单;集合只有一个元素,表示单行菜单;集合有多个元素,表示多选菜单。
// Item context menus
Table(modelData.places, selection: $selection, sortOrder: $sortOrder) {
...
}
.contextMenu(forSelectionType: Place.ID.self) { items in
if items.isEmpty {
// Empty area
AddPlaceButton()
} else {
if items.count == 1 {
// Single item
FavoriteButton(isSet: $modelData.places[items.first!].isFavorite)
}
// Single and multiple items
AddToGuideButton(items)
}
}
关键点:
contextMenu(forSelectionType: Place.ID.self):选择类型要和 Table 的 selection 类型一致。{ items in ... }:闭包拿到本次菜单要作用的 identifier 集合。if items.isEmpty:空白区域菜单适合放“新增地点”这类全局动作。if items.count == 1:单项菜单可以放 favorite 这种只对单个对象成立的动作。AddToGuideButton(items):单选和多选都能复用批量动作。
如果空集合分支没有生成任何 view,SwiftUI 就不会在空白区域显示菜单。这让空白区域菜单可以按需求开启。
用 NavigationSplitView 建立 iPad 结构
(13:52)iPad 的大屏适合同时展示更多层级。SwiftUI 在 iPadOS 16 和 macOS Ventura 改进了 split view 支持,新增 NavigationSplitView,支持两栏和三栏。
两栏布局里,leading column 叫 sidebar,trailing column 叫 detail。横屏默认并排显示。竖屏时,sidebar 收起,detail 保持可见。prominentDetail 会更偏向 detail,balanced 会平衡两栏权重。
三栏布局增加 content column。横屏下默认显示 content 和 detail,sidebar 可切换。竖屏下先显示 detail,再通过按钮依次展开 content 和 sidebar。session 建议三栏布局优先使用 automatic style,因为它会按可用空间做适配。
(16:55)最小的两栏 split view 代码如下。
// Navigation Split View example
struct ContentView: View {
var body: some View {
NavigationSplitView {
SidebarView()
} detail: {
Text("Select a place")
}
}
}
关键点:
NavigationSplitView:声明一个 SwiftUI split view。SidebarView():第一个闭包是 sidebar column。detail::第二个闭包是 detail column。Text("Select a place"):没有从 sidebar 展示内容时,用占位视图填充 detail。
(17:24)进入 Slide Over 或 compact size class 后,columns 会自动折叠成 stack。这样,同一套结构可以覆盖 iPad 横屏、竖屏和窄窗口。
核心启发
-
做什么:把资料库、任务库、地点库从单列列表改成 Table。 为什么值得做:iPadOS 16 的 SwiftUI Table 支持多列、排序和 section,能提高信息密度。 怎么开始:先保留原来的 cell 作为第一列,再用
TableColumn添加可排序字段,并在onChange(of: sortOrder)中重排数据源。 -
做什么:为批量整理场景加入轻量级多选。 为什么值得做:键盘用户能直接用 Shift、Command 多选,触控用户可以通过 edit mode 完成同一流程。 怎么开始:为 Table 增加
@State private var selection: Set<Model.ID>,把$selection传给Table,再在 toolbar 中根据selection.isEmpty显示批量按钮。 -
做什么:给空白区域、单项、多项分别设计上下文菜单。 为什么值得做:multi-select context menus 能让动作靠近用户当前选择,减少来回移动到 toolbar 的次数。 怎么开始:使用
contextMenu(forSelectionType:),在闭包中根据items.isEmpty和items.count选择菜单内容。 -
做什么:把主流程从弹窗改成 sidebar + detail。 为什么值得做:
NavigationSplitView可以在横屏展示更多信息,在竖屏和 Slide Over 自动收起,不需要为每种尺寸单独写结构。 怎么开始:先搭两栏NavigationSplitView,把集合入口放进 sidebar,把当前内容和空状态放进 detail。
关联 Session
- The SwiftUI cookbook for navigation — 深入讲解本场提到的 state restoration、deep linking 和程序化导航。
- SwiftUI on iPad: Add toolbars, titles, and more — 本场的第二部分,继续完善 iPad 上的 SwiftUI 工具栏和标题体验。
- Build a desktop-class iPad app — 从 UIKit 角度讲桌面级 iPad App,与本场的 SwiftUI 路线互补。
- Adopt desktop-class editing interactions — 延伸讲编辑、选择和菜单等桌面级交互。
评论
GitHub Issues · utterances