一个用于表格数据的多平台 SwiftUI 组件。
作为一个开源库提供,可集成到 SwiftUI 应用中。
SwiftTabular 是 OpenAlloc 开源 Swift 软件工具系列的一部分。
macOS | iOS |
---|---|
![]() |
![]() |
RandomAccessCollection
数据源的表格数据AnyView
),这可能会影响可扩展性和性能**支持三种表格类型,由其表头和行的渲染机制决定。
List
ScrollView
/LazyVStack
ScrollView
/LazyVGrid
* 其他平台如 macCatalyst、Mac 上的 iPad、watchOS、tvOS 等支持不佳,甚至完全不支持。 欢迎贡献以改进支持!
** AnyView
仅用于在配置中指定排序图像,这不应影响可扩展性。
下面的示例展示了如何使用 TablerList
(一个基于 List
的简单变体) 从值数组中显示表格数据。
import SwiftUI
import Tabler
struct Fruit: Identifiable {
var id: String
var name: String
var weight: Double
var color: Color
}
struct ContentView: View {
@State private var fruits: [Fruit] = [
Fruit(id: "🍌", name: "Banana", weight: 118, color: .brown),
Fruit(id: "🍓", name: "Strawberry", weight: 12, color: .red),
Fruit(id: "🍊", name: "Orange", weight: 190, color: .orange),
Fruit(id: "🥝", name: "Kiwi", weight: 75, color: .green),
Fruit(id: "🍇", name: "Grape", weight: 7, color: .purple),
Fruit(id: "🫐", name: "Blueberry", weight: 2, color: .blue),
]
private var gridItems: [GridItem] = [
GridItem(.flexible(minimum: 35, maximum: 40), alignment: .leading),
GridItem(.flexible(minimum: 100), alignment: .leading),
GridItem(.flexible(minimum: 40, maximum: 80), alignment: .trailing),
GridItem(.flexible(minimum: 35, maximum: 50), alignment: .leading),
]
private typealias Context = TablerContext<Fruit>
private func header(ctx: Binding<Context>) -> some View {
LazyVGrid(columns: gridItems) {
Text("ID")
Text("Name")
Text("Weight")
Text("Color")
}
}
private func row(fruit: Fruit) -> some View {
LazyVGrid(columns: gridItems) {
Text(fruit.id)
Text(fruit.name).foregroundColor(fruit.color)
Text(String(format: "%.0f g", fruit.weight))
Image(systemName: "rectangle.fill").foregroundColor(fruit.color)
}
}
var body: some View {
TablerList(header: header,
row: row,
results: fruits)
}
}
虽然这里使用 LazyVGrid
来包裹表头和行项目,但您也可以选择使用 HStack
或类似的机制来包裹它们。
Tabler 提供了二十七 (27) 种表格视图变体供您选择。 它们可以按以下方式分类:
List
ScrollView
/LazyVStack
ScrollView
/LazyVGrid
TextField
等)来修改模型config.filter
(请参阅下面的注意事项)表格视图 | 类型 | 选择 | 值 | 引用 | 绑定 | 筛选 |
---|---|---|---|---|---|---|
TablerList |
列表 (List) | ✓ | ✓ | ✓ | ||
TablerListB |
列表 (List) | ✓ | ✓ | ✓* | ||
TablerListC |
列表 (List) | ✓ | ✓ | |||
TablerList1 |
列表 (List) | 单选 (Single) | ✓ | ✓ | ✓ | |
TablerList1B |
列表 (List) | 单选 (Single) | ✓ | ✓ | ✓* | |
TablerList1C |
列表 (List) | 单选 (Single) | ✓ | ✓ | ||
TablerListM |
列表 (List) | 多选 (Multi) | ✓ | ✓ | ✓ | |
TablerListMB |
列表 (List) | 多选 (Multi) | ✓ | ✓ | ✓* | |
TablerListMC |
列表 (List) | 多选 (Multi) | ✓ | ✓ | ||
TablerStack |
堆叠 (Stack) | ✓ | ✓ | ✓ | ||
TablerStackB |
堆叠 (Stack) | ✓ | ✓ | ✓* | ||
TablerStackC |
堆叠 (Stack) | ✓ | ✓ | |||
TablerStack1 |
堆叠 (Stack) | 单选 (Single) | ✓ | ✓ | ✓ | |
TablerStack1B |
堆叠 (Stack) | 单选 (Single) | ✓ | ✓ | ✓* | |
TablerStack1C |
堆叠 (Stack) | 单选 (Single) | ✓ | ✓ | ||
TablerStackM |
堆叠 (Stack) | 多选 (Multi) | ✓ | ✓ | ✓ | |
TablerStackMB |
堆叠 (Stack) | 多选 (Multi) | ✓ | ✓ | ✓* | |
TablerStackMC |
堆叠 (Stack) | 多选 (Multi) | ✓ | ✓ | ||
TablerGrid |
网格 (Grid) | ✓ | ✓ | ✓ | ||
TablerGridB |
网格 (Grid) | ✓ | ✓ | |||
TablerGridC |
网格 (Grid) | ✓ | ✓ | |||
TablerGrid1 |
网格 (Grid) | 单选 (Single) | ✓ | ✓ | ✓ | |
TablerGrid1B |
网格 (Grid) | 单选 (Single) | ✓ | ✓ | ||
TablerGrid1C |
网格 (Grid) | 单选 (Single) | ✓ | ✓ | ||
TablerGridM |
网格 (Grid) | 多选 (Multi) | ✓ | ✓ | ✓ | |
TablerGridMB |
网格 (Grid) | 多选 (Multi) | ✓ | ✓ | ||
TablerGridMC |
网格 (Grid) | 多选 (Multi) | ✓ | ✓ |
* 绑定值的筛选功能可能因目前的实现方式而导致可扩展性不佳。 如果您能找到更好的实现方式,请提交 pull request!
您可以选择为表格附加表头(或表尾)
var body: some View {
TablerList(header: header,
footer: footer,
row: row,
results: fruits)
}
private func header(ctx: Binding<Context>) -> some View {
LazyVGrid(columns: gridItems) {
Text("ID")
Text("Name")
Text("Weight")
Text("Color")
}
}
private func footer(ctx: Binding<Context>) -> some View {
LazyVGrid(columns: gridItems) {
Text("ID")
Text("Name")
Text("Weight")
Text("Color")
}
}
如果您不需要表头(或表尾),只需在表格的声明中省略即可。
对于基于 列表 (List) 的变体,表头和表尾位于滚动区域内部。 对于基于 堆叠 (Stack) 和 网格 (Grid) 的变体,它们位于外部。 (一旦任何缩放/性能问题得到解决,这可能会在某个时候变为可配置。)
列排序可通过 tablerSort
视图函数获得。
下面的示例展示了表头项目如何支持排序。
.columnTitle()
是一个方便的函数,它显示表头名称以及指示当前排序状态的指示器(如果有)。 或者,构建您自己的表头并调用 .indicator()
方法以获取活动指示器图像。
默认情况下,箭头图像用作指示器,但可以配置(请参阅下面的“配置”部分)。
来自 TablerDemo 应用
private typealias Context = TablerContext<Fruit>
private typealias Sort = TablerSort<Fruit>
private func header(ctx: Binding<Context>) -> some View {
LazyVGrid(columns: gridItems) {
Sort.columnTitle("ID", ctx, \.id)
.onTapGesture { tablerSort(ctx, &fruits, \.id) { $0.id < $1.id } }
Sort.columnTitle("Name", ctx, \.name)
.onTapGesture { tablerSort(ctx, &fruits, \.name) { $0.name < $1.name } }
Sort.columnTitle("Weight", ctx, \.weight)
.onTapGesture { tablerSort(ctx, &fruits, \.weight) { $0.weight < $1.weight } }
Text("Color")
}
}
用于 Core Data 的排序方法有所不同。 来自 TablerCoreDemo 应用
private typealias Context = TablerContext<Fruit>
private typealias Sort = TablerSort<Fruit>
private func header(ctx: Binding<Context>) -> some View {
LazyVGrid(columns: gridItems, alignment: .leading) {
Sort.columnTitle("ID", ctx, \.id)
.onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.id)] }
Sort.columnTitle("Name", ctx, \.name)
.onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.name)] }
Sort.columnTitle("Weight", ctx, \.weight)
.onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.weight)] }
}
}
如果某个计算值没有可用于存储在排序上下文中的键路径,请创建一个占位符键路径。
extension Holding {
func getMarketValue(_ priceMap: [String: Double]) -> Double {
shareCount * (priceMap[ticker] ?? 0)
}
var marketValuePlaceholder: Double { 0 }
}
struct HoldingsTable: View {
private typealias Context = TablerContext<Holding>
private func header(_ ctx: Binding<Context>) -> some View {
LazyVGrid(columns: gridItems) {
// ...
Sort.columnTitle("Market Value", ctx, \.marketValuePlaceholder)
.onTapGesture {
tablerSort(ctx, &model.holdings, \.marketValuePlaceholder) {
$0.getMarketValue(priceMap) < $1.getMarketValue(priceMap)
}
}
}
}
}
macOS | iOS |
---|---|
![]() |
![]() |
当与“绑定”视图(例如,TablerListB
或 TablerListC
)一起使用时,可以直接修改数据,从而更改您的数据源。 来自演示:
private func brow(fruit: BoundValue) -> some View {
LazyVGrid(columns: gridItems) {
Text(fruit.wrappedValue.id)
TextField("Name", text: fruit.name)
.textFieldStyle(.roundedBorder)
Text(String(format: "%.0f g", fruit.wrappedValue.weight))
ColorPicker("Color", selection: fruit.color)
.labelsHidden()
}
}
对于值源,BoundValue
是一个绑定 (binding)
typealias BoundValue = Binding<Fruit>
对于引用源,包括 Core Data,BoundValue
是一个对象包装器(又名“ProjectedValue”)
typealias BoundValue = ObservedObject<Fruit>.Wrapper
请注意,对于 Core Data,用户的更改需要保存到托管对象上下文 (Managed Object Context) 中。 有关如何执行此操作的示例,请参阅 TablerCoreDemo 代码。
您可以选择指定行背景,例如为了传达信息,或作为选择指示器。
顾名思义,行背景位于行的后面。
macOS | iOS |
---|---|
![]() |
![]() |
使用行背景来传达信息的示例,如上所示
var body: some View {
TablerList(header: header,
row: row,
rowBackground: rowBackground,
results: fruits)
}
private func rowBackground(fruit: Fruit) -> some View {
LinearGradient(gradient: .init(colors: [fruit.color, fruit.color.opacity(0.2)]),
startPoint: .top,
endPoint: .bottom)
}
使用行背景作为选择指示器的示例,例如对于没有原生选择指示器的基于 堆叠 (Stack) 的表格
@State private var selected: Fruit.ID? = nil
var body: some View {
TablerStack1(header: header,
row: row,
rowBackground: rowBackground,
results: fruits,
selected: $selected)
}
private func rowBackground(fruit: Fruit) -> some View {
RoundedRectangle(cornerRadius: 5)
.fill(fruit.id == selected ? Color.accentColor : Color.clear)
}
与行背景类似,叠加层可用于传达信息,或用作选择指示器。
顾名思义,行叠加层位于行的上面。
使用行叠加层作为选择指示器的示例
@State private var selected: Fruit.ID? = nil
var body: some View {
TablerStack1(header: header,
row: row,
rowOverlay: rowOverlay,
results: fruits,
selected: $selected)
}
private func rowOverlay(fruit: Fruit) -> some View {
RoundedRectangle(cornerRadius: 5)
.strokeBorder(fruit.id == selected ? .white : .clear,
lineWidth: 2,
antialiased: true)
}
仅对于 macOS,您可以捕获悬停事件,通常用于高亮显示鼠标光标下的行。
@State private var hovered: Fruit.ID? = nil
var body: some View {
TablerList(.init(onHover: hoverAction),
header: header,
row: row,
rowBackground: rowBackground,
results: fruits)
}
private func rowBackground(fruit: Fruit) -> some View {
RoundedRectangle(cornerRadius: 5)
.fill(Color.accentColor.opacity(hovered == fruit.id ? 0.2 : 0.0))
}
private func hoverAction(fruitID: Fruit.ID, isHovered: Bool) {
if isHovered { hovered = fruitID } else { hovered = nil }
}
要协调悬停与其他背景(例如,用于 堆叠 (Stack) 表格上的选择)的效果,请参阅演示应用。
基于 列表 (List) 的变体支持通过拖放移动行。
在 TablerDemo 中看到的用于随机访问集合的示例
var body: some View {
TablerList(.init(onMove: moveAction),
row: row,
results: fruits)
}
private func moveAction(from source: IndexSet, to destination: Int) {
fruits.move(fromOffsets: source, toOffset: destination)
}
TODO 需要 Core Data 示例,如果可以做到的话。
var body: some View {
TablerList(.init(onMove: moveAction,
filter: { $0.weight > 10 },
onHover: hoverAction),
header: header,
row: row,
results: fruits)
}
配置选项将因表格类型而异。
默认值可能因平台(macOS、iOS 等)而异。 请参阅代码了解具体信息。
间距默认值旨在实现表格类型之间外观的统一,其中 列表 (List) 类型作为标准。
基本默认值在 TablerConfig
模块中定义。
tablePadding: EdgeInsets
- 无内边距sortIndicatorForward: AnyView
- “chevron.up” 图像sortIndicatorReverse: AnyView
- “chevron.down” 图像sortIndicatorNeutral: AnyView
- “chevron.up” 图像,不透明度为 0列表 (List) 配置是可选的。
TablerListConfig<Element>.init
参数
canMove: CanMove<Element>
- 默认值为 { _ in true }
,允许移动任何行(如果定义了 onMove
)canDelete: CanDelete<Element>
- 默认值为 { _ in true }
,允许删除任何行(如果定义了 onDelete
),目前仅通过 iOS 上的滑动菜单onMove: OnMove<Element>?
- 默认值为 nil
,禁止任何移动onDelete: OnDelete<Element>?
- 默认值为 nil
,禁止任何删除,目前仅通过 iOS 上的滑动菜单filter: Filter?
- 默认值为 nil
,表示不进行筛选onHover: (Element.ID, Bool) -> Void
- 默认为 { _,_ in }
tablePadding: EdgeInsets
- 每个基本默认值sortIndicatorForward: AnyView
- 每个基本默认值sortIndicatorReverse: AnyView
- 每个基本默认值sortIndicatorNeutral: AnyView
- 每个基本默认值堆叠 (Stack) 配置是可选的。
TablerStackConfig<Element>.init
参数
rowPadding: EdgeInsets
- 堆叠 (Stack) 特定的默认值;因平台而异headerSpacing: CGFloat
- 默认值因平台而异footerSpacing: CGFloat
- 默认值因平台而异rowSpacing: CGFloat
- 默认值为 0filter: Filter?
- 默认值为 nil
,表示不进行筛选onHover: (Element.ID, Bool) -> Void
- 默认为 { _,_ in }
tablePadding: EdgeInsets
- 默认值因平台而异sortIndicatorForward: AnyView
- 每个基本默认值sortIndicatorReverse: AnyView
- 每个基本默认值sortIndicatorNeutral: AnyView
- 每个基本默认值网格 (Grid) 配置是必需的,您需要提供 GridItem
数组。
TablerGridConfig<Element>.init
参数
gridItems: [GridItem]
- 必需alignment: HorizontalAlignment
- LazyVGrid
对齐方式,默认值为 .leading
itemPadding: EdgeInsets
- 网格 (Grid) 特定的默认值,因平台而异headerSpacing: CGFloat
- 默认值因平台而异footerSpacing: CGFloat
- 默认值因平台而异rowSpacing: CGFloat
- 默认值为 0filter: Filter?
- 默认值为 nil
,表示不进行筛选onHover: (Element.ID, Bool) -> Void
- 默认为 { _,_ in }
tablePadding: EdgeInsets
- 默认值因平台而异sortIndicatorForward: AnyView
- 每个基本默认值sortIndicatorReverse: AnyView
- 每个基本默认值sortIndicatorNeutral: AnyView
- 每个基本默认值在紧凑型显示器上,您可能希望水平滚动表格。
您可以将其包裹在您自己的 ScrollView
中,或者导入 SwiftSideways 包
import Tabler
import Sideways
var body: some View {
TablerList(header: header,
row: row,
results: fruits)
.sideways(minWidth: 400)
}
这仅适用于那些 fork 并自定义 Tabler 代码的人员。
每个表格变体的许多附加 init()
函数都是通过代码模板 Templates/AutoInit.stencil
生成的。
要重新生成和重新格式化,请从项目目录运行 Sourcery 命令。
$ brew install sourcery
$ sourcery
$ brew install swiftformat
$ swiftformat **/*.swift
生成的代码将在 Sources/Generated
目录中找到。
演示 Tabler 的应用
此库是 OpenAlloc Project 的成员。
版权所有 2021, 2022 OpenAlloc LLC
根据 Apache License, Version 2.0(“许可证”)获得许可; 除非遵守许可证,否则您不得使用此文件。 您可以在以下位置获得许可证副本:
https://apache.ac.cn/licenses/LICENSE-2.0
除非适用法律要求或书面同意,否则根据许可证分发的软件按“原样”基础分发,不附带任何形式的明示或暗示的保证或条件。 有关许可证下管理权限和限制的具体语言,请参阅许可证。
欢迎贡献。 鼓励您提交 pull request 以修复错误、改进文档或提供新功能。
pull request 不需要是生产就绪的功能或修复。 它可以是拟议更改的草稿,或者只是一个测试,以表明预期的行为存在错误。 关于 pull request 的讨论可以从那里开始。