WWDC Quick Look 💓 By SwiftGGTeam
SwiftUI on iPad: Organize your interface

SwiftUI on iPad: Organize your interface

观看原视频

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.isEmptyitems.count 选择菜单内容。

  • 做什么:把主流程从弹窗改成 sidebar + detail。 为什么值得做NavigationSplitView 可以在横屏展示更多信息,在竖屏和 Slide Over 自动收起,不需要为每种尺寸单独写结构。 怎么开始:先搭两栏 NavigationSplitView,把集合入口放进 sidebar,把当前内容和空状态放进 detail。

关联 Session

评论

GitHub Issues · utterances