SwiftUI on iPad: Organize your interface
Swift & UI 进阶 20m

在 iPad 上用 SwiftUI 组织界面

SwiftUI on iPad: Organize your interface

2022年6月6日

在 Apple 官方观看视频

一句话判断

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
WWDC 2022