表格
深入理解 SwiftUI 的 Table 组件,掌握创建可选择、可排序的多列数据表格
SwiftUI 的 Table 组件提供了一种强大的方式来展示结构化的多列数据,支持选择、排序、自定义列等功能。本文详细介绍 SwiftUI 表格的核心概念和使用方法。
概述
Table 是一个容器视图,用于以行列形式展示数据集合。每个集合元素对应表格中的一行,每个属性值对应不同的列。
主要特性
- 自动提供垂直滚动(macOS 上还支持水平滚动)
- 支持单选和多选
- 支持列排序
- 支持列自定义(重排序、显示/隐藏)
- 支持层级数据展示
- 在窄屏幕上自动适配为类似 List 的外观
平台支持
- iOS 16.0+
- iPadOS 16.0+
- macOS 12.0+
- Mac Catalyst 16.0+
- visionOS 1.0+
在 iOS 上,当 horizontalSizeClass 为 compact 时,表格可能只显示第一列,其他列会被隐藏。
创建基础表格
简单表格示例
首先定义数据模型:
struct Person: Identifiable {
let givenName: String
let familyName: String
let emailAddress: String
let id = UUID()
var fullName: String { givenName + " " + familyName }
}创建表格:
struct PeopleTable: View {
@State private var people = [
Person(givenName: "Juan", familyName: "Chavez", emailAddress: "juanchavez@icloud.com"),
Person(givenName: "Mei", familyName: "Chen", emailAddress: "meichen@icloud.com"),
Person(givenName: "Tom", familyName: "Clark", emailAddress: "tomclark@icloud.com"),
Person(givenName: "Gita", familyName: "Kumar", emailAddress: "gitakumar@icloud.com")
]
var body: some View {
Table(people) {
TableColumn("Given Name", value: \.givenName)
TableColumn("Family Name", value: \.familyName)
TableColumn("E-Mail Address", value: \.emailAddress)
}
}
}这个示例创建了一个包含三列的表格,每列显示 Person 的一个属性。
表格列 (TableColumn)
TableColumn 定义了表格中的一列,包括列标题、数据来源和显示方式。
不可排序的列
最简单的列定义,仅用于显示:
TableColumn("Name") { person in
Text(person.fullName)
.font(.headline)
}可排序的列
使用 value 参数指定用于排序的 KeyPath:
// String 属性的便捷形式
TableColumn("Given Name", value: \.givenName)
// 自定义内容视图
TableColumn("Given Name", value: \.givenName) { person in
Text(person.givenName)
.foregroundColor(.blue)
}自定义排序比较器
对于非标准类型或自定义排序逻辑:
struct Report: Identifiable {
let id = UUID()
let title: String
let priority: Priority
enum Priority: Comparable {
case low, medium, high
}
}
TableColumn("Priority", value: \.priority, comparator: { lhs, rhs in
lhs.rawValue < rhs.rawValue
}) { report in
PriorityBadge(priority: report.priority)
}列宽度控制
固定宽度
TableColumn("ID", value: \.id)
.width(80)可调整宽度
TableColumn("Description", value: \.description)
.width(min: 100, ideal: 200, max: 400)列对齐
TableColumn("Amount", value: \.amount) { item in
Text(item.amount, format: .currency(code: "USD"))
}
.alignment(.trailing)选择功能
单选
绑定到单个可选的 ID 值:
struct SelectableTable: View {
@State private var people = [/* ... */]
@State private var selectedPerson: Person.ID?
var body: some View {
Table(people, selection: $selectedPerson) {
TableColumn("Given Name", value: \.givenName)
TableColumn("Family Name", value: \.familyName)
TableColumn("E-Mail", value: \.emailAddress)
}
}
}多选
绑定到 Set<ID>:
struct MultiSelectTable: View {
@State private var people = [/* ... */]
@State private var selectedPeople: Set<Person.ID> = []
var body: some View {
VStack {
Table(people, selection: $selectedPeople) {
TableColumn("Given Name", value: \.givenName)
TableColumn("Family Name", value: \.familyName)
TableColumn("E-Mail", value: \.emailAddress)
}
Text("已选择 \(selectedPeople.count) 人")
.padding()
}
}
}排序功能
基础排序
使用 sortOrder 绑定启用排序:
struct SortableTable: View {
@State private var people = [/* ... */]
@State private var sortOrder = [KeyPathComparator(\Person.givenName)]
var body: some View {
Table(people, sortOrder: $sortOrder) {
TableColumn("Given Name", value: \.givenName)
TableColumn("Family Name", value: \.familyName)
TableColumn("E-Mail", value: \.emailAddress)
}
.onChange(of: sortOrder) { oldValue, newValue in
people.sort(using: newValue)
}
}
}多列排序
用户可以通过按住 Shift 键点击列标题来添加次要排序条件:
@State private var sortOrder = [
KeyPathComparator(\Person.familyName),
KeyPathComparator(\Person.givenName)
]自定义排序比较器
struct CustomSortTable: View {
@State private var items = [/* ... */]
@State private var sortOrder = [KeyPathComparator(\Item.date)]
var body: some View {
Table(items, sortOrder: $sortOrder) {
TableColumn("Name", value: \.name)
TableColumn("Date", sortUsing: KeyPathComparator(\.date)) { item in
Text(item.date, style: .date)
}
TableColumn("Status", value: \.status, comparator: StatusComparator())
}
.onChange(of: sortOrder) { _, newOrder in
items.sort(using: newOrder)
}
}
}
struct StatusComparator: SortComparator {
typealias Compared = Status
func compare(_ lhs: Status, _ rhs: Status) -> ComparisonResult {
// 自定义比较逻辑
lhs.priority.compare(to: rhs.priority)
}
}列自定义 (TableColumnCustomization)
从 iOS 17+ / macOS 14+ 开始,表格支持列的重排序和显示/隐藏。
启用列自定义
struct CustomizableTable: View {
@State private var bugReports = [/* ... */]
@State private var selectedReports: Set<BugReport.ID> = []
@State private var sortOrder = [KeyPathComparator(\BugReport.title)]
@SceneStorage("BugReportTableConfig")
private var columnCustomization: TableColumnCustomization<BugReport>
var body: some View {
Table(
bugReports,
selection: $selectedReports,
sortOrder: $sortOrder,
columnCustomization: $columnCustomization
) {
TableColumn("Title", value: \.title)
.customizationID("title")
TableColumn("ID", value: \.id) { report in
Link("\(report.id)", destination: report.url)
}
.customizationID("id")
TableColumn("Reports", value: \.duplicateCount) { report in
Text(report.duplicateCount, format: .number)
}
.customizationID("duplicates")
}
.onChange(of: sortOrder) { _, newOrder in
bugReports.sort(using: newOrder)
}
}
}编程方式控制列可见性
// 隐藏某一列
columnCustomization[visibility: "duplicates"] = .hidden
// 显示某一列
columnCustomization[visibility: "duplicates"] = .visible
// 自动(使用默认可见性)
columnCustomization[visibility: "duplicates"] = .automatic绑定到列可见性
struct ColumnVisibilityControl: View {
@SceneStorage("TableConfig")
private var columnCustomization: TableColumnCustomization<Item>
var body: some View {
VStack {
// 表格
Table(/* ... */, columnCustomization: $columnCustomization) {
// 列定义
}
// 可见性控制
Toggle(
"显示重复数量",
isOn: Binding(
get: { columnCustomization[visibility: "duplicates"] == .visible },
set: { columnCustomization[visibility: "duplicates"] = $0 ? .visible : .hidden }
)
)
}
}
}持久化配置
使用 @SceneStorage 或 @AppStorage 保存列配置:
// 场景级别持久化
@SceneStorage("MyTableConfig")
private var columnCustomization: TableColumnCustomization<MyData>
// 应用级别持久化
@AppStorage("MyTableConfig")
private var columnCustomization: TableColumnCustomization<MyData>每个可自定义的列必须设置唯一的 customizationID,否则该列将不可自定义。
表格行
TableRow
TableRow 用于在更复杂的场景中显式定义行:
Table(of: Person.self) {
TableColumn("Name", value: \.fullName)
TableColumn("Email", value: \.emailAddress)
} rows: {
ForEach(people) { person in
TableRow(person)
}
}DisclosureTableRow
用于创建可展开/折叠的层级表格(iOS 17+ / macOS 14+):
struct FileItem: Identifiable {
let id = UUID()
let name: String
let size: Int64
var children: [FileItem]?
}
struct HierarchicalTable: View {
@State private var files = [/* ... */]
@State private var expandedItems: Set<FileItem.ID> = []
var body: some View {
Table(of: FileItem.self) {
TableColumn("Name", value: \.name)
TableColumn("Size") { file in
Text(file.size, format: .byteCount(style: .file))
}
} rows: {
ForEach(files) { file in
buildRow(for: file)
}
}
}
@ViewBuilder
private func buildRow(for file: FileItem) -> some View {
if let children = file.children {
DisclosureTableRow(
file,
isExpanded: Binding(
get: { expandedItems.contains(file.id) },
set: { isExpanded in
if isExpanded {
expandedItems.insert(file.id)
} else {
expandedItems.remove(file.id)
}
}
)
) {
ForEach(children) { child in
buildRow(for: child)
}
}
} else {
TableRow(file)
}
}
}层级表格
使用 children 参数创建层级表格:
struct FolderItem: Identifiable {
let id = UUID()
let name: String
let size: Int64
var children: [FolderItem]?
}
struct HierarchicalFolderTable: View {
@State private var folders: [FolderItem] = [
FolderItem(name: "Documents", size: 1024, children: [
FolderItem(name: "Work", size: 512, children: nil),
FolderItem(name: "Personal", size: 512, children: nil)
]),
FolderItem(name: "Downloads", size: 2048, children: nil)
]
var body: some View {
Table(folders, children: \.children) {
TableColumn("Name", value: \.name)
TableColumn("Size") { folder in
Text(folder.size, format: .byteCount(style: .file))
}
}
}
}表格样式 (TableStyle)
内置样式
automatic
默认样式,根据上下文自动选择:
Table(people) {
// 列定义
}
.tableStyle(.automatic)inset
内容和选择背景内嵌的样式:
Table(people) {
// 列定义
}
.tableStyle(.inset)bordered
带标准边框的样式(仅 macOS):
Table(people) {
// 列定义
}
.tableStyle(.bordered)自定义表格样式
实现 TableStyle 协议创建自定义样式:
struct CustomTableStyle: TableStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.content
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.padding()
}
}
extension TableStyle where Self == CustomTableStyle {
static var custom: CustomTableStyle { CustomTableStyle() }
}
// 使用
Table(people) {
// 列定义
}
.tableStyle(.custom)紧凑布局适配
在 iOS 上,表格会根据水平尺寸类别自动调整显示:
struct AdaptiveTable: View {
#if os(iOS)
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
private var isCompact: Bool { horizontalSizeClass == .compact }
#else
private let isCompact = false
#endif
@State private var people = [/* ... */]
@State private var sortOrder = [KeyPathComparator(\Person.givenName)]
var body: some View {
Table(people, sortOrder: $sortOrder) {
TableColumn("Given Name", value: \.givenName) { person in
VStack(alignment: .leading) {
Text(isCompact ? person.fullName : person.givenName)
if isCompact {
Text(person.emailAddress)
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
TableColumn("Family Name", value: \.familyName)
TableColumn("E-Mail", value: \.emailAddress)
}
.onChange(of: sortOrder) { _, newOrder in
people.sort(using: newOrder)
}
}
}这种方式在紧凑模式下将所有信息合并到第一列,提供类似列表的外观。
实践示例
完整的任务管理表格
struct Task: Identifiable {
let id = UUID()
var title: String
var priority: Priority
var dueDate: Date
var isCompleted: Bool
enum Priority: String, Comparable, CaseIterable {
case low = "低"
case medium = "中"
case high = "高"
static func < (lhs: Priority, rhs: Priority) -> Bool {
let order: [Priority] = [.low, .medium, .high]
return order.firstIndex(of: lhs)! < order.firstIndex(of: rhs)!
}
}
}
struct TaskTableView: View {
@State private var tasks = [
Task(title: "完成报告", priority: .high, dueDate: Date(), isCompleted: false),
Task(title: "回复邮件", priority: .medium, dueDate: Date().addingTimeInterval(86400), isCompleted: false),
Task(title: "整理文档", priority: .low, dueDate: Date().addingTimeInterval(172800), isCompleted: true)
]
@State private var selectedTasks: Set<Task.ID> = []
@State private var sortOrder = [KeyPathComparator(\Task.dueDate)]
@SceneStorage("TaskTableConfig")
private var columnCustomization: TableColumnCustomization<Task>
var body: some View {
VStack {
Table(
tasks,
selection: $selectedTasks,
sortOrder: $sortOrder,
columnCustomization: $columnCustomization
) {
TableColumn("标题", value: \.title)
.customizationID("title")
TableColumn("优先级", value: \.priority) { task in
HStack {
Circle()
.fill(priorityColor(task.priority))
.frame(width: 8, height: 8)
Text(task.priority.rawValue)
}
}
.customizationID("priority")
.width(100)
TableColumn("截止日期", value: \.dueDate) { task in
Text(task.dueDate, style: .date)
}
.customizationID("dueDate")
.width(120)
TableColumn("状态") { task in
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(task.isCompleted ? .green : .gray)
}
.customizationID("status")
.width(60)
}
.onChange(of: sortOrder) { _, newOrder in
tasks.sort(using: newOrder)
}
HStack {
Text("已选择 \(selectedTasks.count) 个任务")
Spacer()
Button("删除选中") {
tasks.removeAll { selectedTasks.contains($0.id) }
selectedTasks.removeAll()
}
.disabled(selectedTasks.isEmpty)
}
.padding()
}
}
private func priorityColor(_ priority: Task.Priority) -> Color {
switch priority {
case .low: return .green
case .medium: return .orange
case .high: return .red
}
}
}层级文件浏览器
struct FileNode: Identifiable {
let id = UUID()
let name: String
let size: Int64
let modifiedDate: Date
var children: [FileNode]?
var isFolder: Bool { children != nil }
}
struct FileExplorerTable: View {
@State private var files: [FileNode] = [
FileNode(
name: "Documents",
size: 0,
modifiedDate: Date(),
children: [
FileNode(name: "Report.pdf", size: 1024000, modifiedDate: Date(), children: nil),
FileNode(name: "Notes.txt", size: 2048, modifiedDate: Date(), children: nil)
]
),
FileNode(
name: "Pictures",
size: 0,
modifiedDate: Date(),
children: [
FileNode(name: "Photo1.jpg", size: 2048000, modifiedDate: Date(), children: nil),
FileNode(name: "Photo2.jpg", size: 1536000, modifiedDate: Date(), children: nil)
]
)
]
@State private var selectedFiles: Set<FileNode.ID> = []
@State private var sortOrder = [KeyPathComparator(\FileNode.name)]
var body: some View {
Table(
files,
children: \.children,
selection: $selectedFiles,
sortOrder: $sortOrder
) {
TableColumn("名称", value: \.name) { file in
HStack {
Image(systemName: file.isFolder ? "folder.fill" : "doc.fill")
.foregroundColor(file.isFolder ? .blue : .gray)
Text(file.name)
}
}
TableColumn("大小", value: \.size) { file in
if file.isFolder {
Text("--")
} else {
Text(file.size, format: .byteCount(style: .file))
}
}
.width(100)
TableColumn("修改日期", value: \.modifiedDate) { file in
Text(file.modifiedDate, style: .date)
}
.width(120)
}
.onChange(of: sortOrder) { _, newOrder in
sortFiles(&files, using: newOrder)
}
}
private func sortFiles(_ files: inout [FileNode], using order: [KeyPathComparator<FileNode>]) {
files.sort(using: order)
for index in files.indices {
if var children = files[index].children {
sortFiles(&children, using: order)
files[index].children = children
}
}
}
}总结
- Table 提供了强大的多列数据展示能力,支持自动滚动
- TableColumn 定义列的显示和排序行为,支持自定义宽度和对齐
- 选择功能支持单选和多选,通过绑定管理选择状态
- 排序功能使用
KeyPathComparator实现,支持多列排序 - TableColumnCustomization 允许用户重排序和显示/隐藏列,配置可持久化
- DisclosureTableRow 和
children参数支持层级数据展示 - TableStyle 提供多种内置样式,也可自定义样式
- 在 iOS 上会根据屏幕尺寸自动适配,紧凑模式下类似 List 的外观
- 适合展示结构化数据,如文件列表、任务管理、数据报表等场景
上次更新于