网格
受 CSS Grid 启发的 SwiftUI 编写的网格视图
我们是一家开发公司,打造惊人的应用程序。
概览
Grid 是在 SwiftUI 中灵活布局视图的有力且简单的方式
下面查看完整文档。
安装
CocoaPods
通过CocoaPods提供网格功能。要安装它,只需将以下行添加到您的Podfile中
pod 'ExyteGrid'
Swift Package Manager
将其添加到现有的Xcode项目中作为包依赖项
- 从文件菜单中,选择Swift Packages › 添加包依赖项…
- 在包仓库URL文本字段中输入 "https://github.com/exyte/Grid"
需求
- iOS 14.0+(最新iOS 13支持在 v0.1.0 中)
- MacOS 10.15+
- Xcode 12+
从源码构建
git clone [email protected]:exyte/Grid.git
cd Grid/Example/
pod install
open Example.xcworkspace/
文档
- 初始化
- 视图容器
- 轨道大小
- 网格单元格背景和覆盖层
- 跨越网格视图
- 按行
- 按列
- 视图位置指定
- 自动(隐式)
- 起始行
- 起始列
- 行和列都指定
- 流动方向
- 内容模式
- 包装模式
- 垂直和水平间距
- 对齐方式
- 内容更新可以动画显示
- 缓存
- 条件语句 / @GridBuilder
- 版本说明
- 路线图
1. 初始化
你可以通过不同的方式实例化网格
- 只需在 ViewBuilder 闭包内指定轨道和视图
Grid(tracks: 3) {
ColorView(.blue)
ColorView(.purple)
ColorView(.red)
ColorView(.cyan)
ColorView(.green)
ColorView(.orange)
}
- 使用范围
Grid(0..<6, tracks: 3) { _ in
ColorView(.random)
}
- 使用可识别实体
Grid(colorModels, tracks: 3) {
ColorView($0)
}
- 使用显式定义的 ID
Grid(colorModels, id: \.self, tracks: 3) {
ColorView($0)
}
2. 容器
ForEach
在 ViewBuilder 里面你也可以使用常规的 ForEach
语句。 从初始化的 ForEach 视图中无法获取 KeyPath id 值。其内部内容将通过视图顺序在动画过程中区分。使用带有 Identifiable
模型的 ForEach
或使用要么具有显式 ID 值,要么是 Identifiable
模型的 GridGroup 来跟踪网格视图及其 View
表示在动画中会更好。
Grid(tracks: 4) {
ColorView(.red)
ColorView(.purple)
ForEach(0..<4) { _ in
ColorView(.black)
}
ColorView(.orange)
ColorView(.green)
}
var arithmeticButtons: GridGroup {
GridGroup {
CalcButton(.divide)
CalcButton(.multiply)
CalcButton(.substract)
CalcButton(.equal)
}
}
var arithmeticButtons: GridGroup {
let operations: [MathOperation] =
[.divide, .multiply, .substract, .add, .equal]
return GridGroup(operations, id: \.self) {
CalcButton($0)
}
}
var arithmeticButtons: GridGroup {
let operations: [MathOperation] =
[.divide, .multiply, .substract, .add, .equal]
return GridGroup {
ForEach(operations, id: \.self) {
CalcButton($0)
}
}
}
var arithmeticButtons: GridGroup {
let operations: [MathOperation] =
[.divide, .multiply, .substract, .add, .equal]
return GridGroup(operations, id: \.self) {
CalcButton($0)
}
}
var arithmeticButtons: GridGroup {
let operations: [MathOperation] =
[.divide, .multiply, .substract, .add, .equal]
return GridGroup(operations, id: \.self) {
CalcButton($0)
}
}
Grid {
...
GridGroup(MathOperation.clear) {
CalcButton($0)
}
}
.pt(N)
其中N - 点数。
Grid(tracks: [.pt(50), .pt(200), .pt(100)]) {
ColorView(.blue)
ColorView(.purple)
ColorView(.red)
ColorView(.cyan)
ColorView(.green)
ColorView(.orange)
}
Grid(0..<6, tracks: [.fit, .fit, .fit]) {
ColorView(.random)
.frame(maxWidth: 50 + 15 * CGFloat($0))
}
请注意限制填满父组件提供的整个空间和 标记为 Text()
的视图的大小,这些视图倾向于作为单行绘制。
.fr(N)
灵活尺寸的轨道:
Fr 是一个分数单位,.fr(1)
表示网格中未分配空间的 1 部分。灵活尺寸的轨道在所有非灵活尺寸轨道(.pt 和 .fit)计算后才进行计算。因此,可分配的空间是他们和未分配轨道大小的总和。
Grid(tracks: [.pt(100), .fr(1), .fr(2.5)]) {
ColorView(.blue)
ColorView(.purple)
ColorView(.red)
ColorView(.cyan)
ColorView(.green)
ColorView(.orange)
}
您也可以将整数量词直接指定为轨道大小。这等于重复 .fr(1)
轨道大小。
Grid(tracks: 3) { ... }
等于
Grid(tracks: [.fr(1), .fr(1), .fr(1)]) { ... }
4. 网格单元的背景和覆盖层
当使用非灵活尺寸的轨道时,可分配的额外空间可能大于网格项目能够处理的。为了填充这个空间,您可以使用 .gridCellBackground(...)
和 gridCellOverlay(...)
修饰符。
5. 横跨
每个网格视图都可以跨越 provided number of grid tracks。您可以使用 .gridSpan(column: row:)
修饰符实现这一点。默认横跨为 1。
横跨的轨道数大于等于 2 且横跨灵活尺寸轨道的视图不参与这些轨道的大小分配。此视图将适合横跨的轨道。因此,可以放置一个视图,要横跨基于内容大小尺寸的轨道(.fit)。
Grid(tracks: [.fr(1), .pt(150), .fr(2)]) {
ColorView(.blue)
.gridSpan(column: 2)
ColorView(.purple)
.gridSpan(row: 2)
ColorView(.red)
ColorView(.cyan)
ColorView(.green)
.gridSpan(column: 2, row: 3)
ColorView(.orange)
ColorView(.magenta)
.gridSpan(row: 2)
}
跨不同尺寸类型的轨道
var body: some View {
Grid(tracks: [.fr(1), .fit, .fit], spacing: 10) {
VCardView(text: placeholderText(),
color: .red)
VCardView(text: placeholderText(length: 30),
color: .orange)
.frame(maxWidth: 70)
VCardView(text: placeholderText(length: 120),
color: .green)
.frame(maxWidth: 100)
.gridSpan(column: 1, row: 2)
VCardView(text: placeholderText(length: 160),
color: .magenta)
.gridSpan(column: 2, row: 1)
VCardView(text: placeholderText(length: 190),
color: .cyan)
.gridSpan(column: 3, row: 1)
}
}
6. 开始
对于每一个视图,你可以通过指定列、行或者两者来设置显式的起始位置。如果没有指定起始位置,视图将被自动定位。首先,具有列和行起始位置的视图将被放置。其次,自动放置算法尝试放置只有列或行起始位置的视图。如果存在冲突,则这些视图将被自动放置,你将在控制台中看到警告。最后,将放置没有指定显式起始位置的视图。
起始位置使用.gridStart(column: row:)
修饰符定义。
Grid(tracks: [.pt(50), .fr(1), .fr(1.5), .fit]) {
ForEach(0..<6) { _ in
ColorView(.black)
}
ColorView(.brown)
.gridSpan(column: 3)
ColorView(.blue)
.gridSpan(column: 2)
ColorView(.orange)
.gridSpan(row: 3)
ColorView(.red)
.gridStart(row: 1)
.gridSpan(column: 2, row: 2)
ColorView(.yellow)
.gridStart(row: 2)
ColorView(.purple)
.frame(maxWidth: 50)
.gridStart(column: 3, row: 0)
.gridSpan(row: 9)
ColorView(.green)
.gridSpan(column: 2, row: 3)
ColorView(.cyan)
ColorView(.gray)
.gridStart(column: 2)
}
7. 流
网格有2种类型的轨道。第一种是您指定轨道大小的地方——固定大小。固定意味着轨道的数量已知。第二种是垂直于固定大小轨道的扩展轨道类型:您的内内容量增长。网格流定义了项目增长的方向
行
默认。列数固定,并由轨道大小定义。网格项沿着列移动并切换到下一行。行数递增。
列
行数是固定的,并被定义为轨道大小。网格项在行之间移动,并在最后一行之后切换到下一列。列数正在增加。
网格布局可以在网格构造函数中指定,也可以使用.gridFlow(...)
网格修饰符。第一种方法具有更高的优先级。
struct ContentView: View {
@State var flow: GridFlow = .rows
var body: some View {
VStack {
if self.flow == .rows {
Button(action: { self.flow = .columns }) {
Text("Flow: ROWS")
}
} else {
Button(action: { self.flow = .rows }) {
Text("Flow: COLUMNS")
}
}
Grid(0..<15, tracks: 5, flow: self.flow, spacing: 5) {
ColorView($0.isMultiple(of: 2) ? .black : .orange)
.overlay(
Text(String($0))
.font(.system(size: 35))
.foregroundColor(.white)
)
}
.animation(.default)
}
}
}
8. 内容模式
有两种内容模式
滚动
在此模式中,内部网格内容可以滚动到增长方向。与网格流方向(增长)垂直的网格轨道隐式假定具有.fit大小。这意味着它们的大小必须在相应的维度中定义。
网格内容模式可以在网格构造函数中指定,也可以使用.gridContentMode(...)
网格修饰符。第一种方法具有更高的优先级。
行流滚动
struct VCardView: View {
let text: String
let color: UIColor
var body: some View {
VStack {
Image("dog")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(5)
.frame(minWidth: 100, minHeight: 50)
Text(self.text)
.layoutPriority(.greatestFiniteMagnitude)
}
.padding(5)
.gridCellBackground { _ in
ColorView(self.color)
}
.gridCellOverlay { _ in
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color(self.color.darker()),
lineWidth: 3)
}
}
}
struct ContentView: View {
var body: some View {
Grid(tracks: 3) {
ForEach(0..<40) { _ in
VCardView(text: randomText(), color: .random)
.gridSpan(column: self.randomSpan)
}
}
.gridContentMode(.scroll)
.gridPacking(.dense)
.gridFlow(.rows)
}
var randomSpan: Int {
Int(arc4random_uniform(3)) + 1
}
}
列流滚动
struct HCardView: View {
let text: String
let color: UIColor
var body: some View {
HStack {
Image("dog")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(5)
Text(self.text)
.frame(maxWidth: 200)
}
.padding(5)
.gridCellBackground { _ in
ColorView(self.color)
}
.gridCellOverlay { _ in
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color(self.color.darker()),
lineWidth: 3)
}
}
}
struct ContentView: View {
var body: some View {
Grid(tracks: 3) {
ForEach(0..<8) { _ in
HCardView(text: randomText(), color: .random)
.gridSpan(row: self.randomSpan)
}
}
.gridContentMode(.scroll)
.gridFlow(.columns)
.gridPacking(.dense)
}
var randomSpan: Int {
Int(arc4random_uniform(3)) + 1
}
}
填充
默认。在此模式下,网格视图试图用其内容填充父视图提供的全部空间。与网格流方向(增长)正交的网格轨道隐式假设为 .fr(1) 大小。
@State var contentMode: GridContentMode = .scroll
var body: some View {
VStack {
self.modesPicker
Grid(models, id: \.self, tracks: 3) {
VCardView(text: $0.text, color: $0.color)
.gridSpan($0.span)
}
.gridContentMode(self.contentMode)
.gridFlow(.rows)
.gridAnimation(.default)
}
}
9 对齐
自动放置算法可能采用两种策略之一
稀疏
默认。放置算法仅在放置项目时始终“向前”移动网格,永远不会回溯以填充空缺。这确保了所有自动放置的项目都按照“顺序”出现,即使这样会留下可能被后来项目填充的空缺。
密集
尝试在网格中较早填满空缺,如果随后出现较小项目。这可能会导致项目出序,当这样做会填充较大项目留下的空缺时。
网格填充可以在网格构造函数以及使用 .gridPacking(...)
网格修饰符中指定。第一种选项的优先级更高。
示例
@State var gridPacking = GridPacking.sparse
var body: some View {
VStack {
self.packingPicker
Grid(tracks: 4) {
ColorView(.red)
ColorView(.black)
.gridSpan(column: 4)
ColorView(.purple)
ColorView(.orange)
ColorView(.green)
}
.gridPacking(self.gridPacking)
.gridAnimation(.default)
}
}
10. 间距
有几种方法可以定义轨之间的水平和垂直间距
- 使用
Int
文字面量表示所有方向上等间距
Grid(tracks: 4, spacing: 5) { ... }
- 使用显式初始化
Grid(tracks: 4, spacing: GridSpacing(horizontal: 10, vertical: 5)) { ... }
- 使用数组文字面量
Grid(tracks: 4, spacing: [10, 5]) { ... }
示例
@State var vSpacing: CGFloat = 0
@State var hSpacing: CGFloat = 0
var body: some View {
VStack {
self.sliders
Grid(tracks: 3, spacing: [hSpacing, vSpacing]) {
ForEach(0..<21) {
//Inner image used to measure size
self.image
.aspectRatio(contentMode: .fit)
.opacity(0)
.gridSpan(column: max(1, $0 % 4))
.gridCellOverlay {
//This one is to display
self.image
.aspectRatio(contentMode: .fill)
.frame(width: $0?.width,
height: $0?.height)
.cornerRadius(5)
.clipped()
.shadow(color: self.shadowColor,
radius: 10, x: 0, y: 0)
}
}
}
.background(self.backgroundColor)
.gridContentMode(.scroll)
.gridPacking(.dense)
}
}
11. 对齐
.gridItemAlignment
用于指定单个网格项的对齐方式。其优先级高于 gridCommonItemsAlignment
.gridCommonItemsAlignment
应用于每个项,但不会覆盖其单独的 gridItemAlignment
值。
.gridContentAlignment
应用于整个网格内容。当内容大小小于网格的可空空间时生效。
示例
struct SingleAlignmentExample: View {
var body: some View {
Grid(tracks: 3) {
TextCardView(text: "Hello", color: .red)
.gridItemAlignment(.leading)
TextCardView(text: "world", color: .blue)
}
.gridCommonItemsAlignment(.center)
.gridContentAlignment(.trailing)
}
}
struct TextCardView: View {
let text: String
let color: UIColor
var textColor: UIColor = .white
var body: some View {
Text(self.text)
.foregroundColor(Color(self.textColor))
.padding(5)
.gridCellBackground { _ in
ColorView(color)
}
.gridCellOverlay { _ in
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color(self.color.darker()),
lineWidth: 3)
}
}
}
12. 动画
您可以使用 .gridAnimation()
网格修饰符定义将应用到内部 ZStack
的特定动画。
默认情况下,网格中的每个视图都与后续索引关联作为它的 ID。因此 SwiftUI 依赖于网格视图在初始状态和最终状态中的位置来执行动画过渡。您可以使用 ForEach 或由 Identifiable
模型初始化的 GridGroup 将特定 ID 关联到网格视图,或通过显式 KeyPath 将 ID 设置为强制动画正确执行。
无法从初始化的 ForEach 视图中获取 KeyPath ID 值。在动画过程中,其内部内容将由视图的顺序来区分。最好使用与 ForEach 的 Identifiable
模型或通过显式 ID 值或 Identifiable
模型创建的 GridGroup 结合使用,以保持对网格视图及其 View
表示在动画中的跟踪。
13. 缓存
可以缓存网格布局,在整个 Grid 生命周期内保持其。
仅支持 iOS。
网格缓存可以在网格构造函数中指定,也可以使用 .gridCaching(...)
网格修饰符进行指定。第一种方法具有更高的优先级。
内存缓存
默认。 缓存通过利用 NSCache 实现。它将在内存警告通知中清除所有缓存的布局。
不缓存
不使用缓存。布局计算将在网格生命周期每个步骤执行。
14. 条件语句 / @GridBuilder
从Swift 5.3开始,我们可以使用自定义函数构建器而无需任何问题。这使我们能够
-
完全支持在网格和网格组体中使用
if/if else
、if let/if let else
、switch
语句。 -
从嵌套的
GridGroup
和ForEach
中传播视图ID的更好方式 -
使用
@GridBuilder
属性和some View
返回类型从函数和变量中返回异构视图的能力
@GridBuilder
func headerSegment(flag: Bool) -> some View {
if flag {
return GridGroup { ... }
else {
return ColorView(.black)
}
}
发布说明
v1.5.0
- 增加tvOS支持,将示例项目迁移到Xcode通用目标,Swift应用程序生命周期,SPM
v1.4.2
- 修复了网格项在垂直轴上不工作的网格项对齐和网格常用项对齐问题
先前版本
##### [v1.4.1](https://github.com/exyte/Grid/releases/tag/1.4.1): - 修复了网格内容没有更新时的问题问题:如果在 GridBuilder 中的任何内容项使用外部数据,则网格不会更新它。例如
@State var titleText: String = "title"
Grid(tracks: 2) {
Text(titleText)
Text("hello")
}
网格没有更新 titleText 即使它已更改。
v1.4.0
- 添加了
gridItemAlignment
修饰符以对齐单个项目 - 添加了
gridCommonItemsAlignment
修饰符以对齐所有项目 - 添加了
gridContentAlignment
修饰符以对齐整个网格内容
v1.3.1.beta
- 添加了
gridAlignment
修饰符以对齐单个项目
v1.2.1.beta
- 为网格中的所有项目添加了
gridCommonItemsAlignment
修饰符以对齐
v1.1.1.beta
- 通过条件渲染ScrollView增加了WidgetKit支持
v1.1.0
- 增加了对MacOS的支持
v1.0.1
- 增加对条件语句的全面支持
- 增加
@GridBuilder
函数构建器
v0.1.0
- 增加了布局缓存
- 增加单例
Identifiable
或Hashable
值来初始化GridGroup
v0.0.3
- 修复了Grid条件呈现时出现的任何问题
- 修复了设备旋转后滚动内容中网格位置的错误
- 修复了 iOS 14 中的“**边界偏好尝试每帧更新多次”警告,并在 iOS 13 中减少了这些警告
- 简化了底层收集网格偏好的过程
v0.0.2
- 增加了对 Swift Package Manager 的支持
路线图
- 添加了 WidgetKit 示例
- 添加了对每条轨道对齐的支持
- 为 GridGroup 添加了指定位置的区域或设置
- 双维度轨道大小(grid-template-rows, grid-template-columns)
- grid-auto-rows, grid-auto-columns
- 改进了密集放置算法
- ? 网格最小/理想大小
- ? 横屏/竖屏布局
- ? 在后台线程计算布局
- 为动画中跟踪相同视图添加类似于 GridIdentified 的项
- 使用函数构造器支持 if 子句
- 添加 GridGroup
- 网格项显式行和/或列位置
- 行和列有不同的间距
- 内部大小轨道(fit-content)
- forEach 支持功能
- 密集/稀疏放置算法
- 添加水平轴
- 通过 Identifiable 模型初始化
- 可滚动的内同
许可
Grid 在 MIT 许可下可用。有关更多信息,请参阅 LICENSE 文件。
我们的其他开源 SwiftUI 库
PopupView - 吐司和弹出窗口库
ScalingHeaderScrollView - 拖动时自动缩放的头部滚动的滚动视图
AnimatedTabBar - 带有预设动画的标签栏
MediaPicker - 可定制的媒体选择器
ConcentricOnboarding - 动画引导流程
FloatingButton - 悬浮按钮菜单
ActivityIndicatorView - 多种动画加载指示器
ProgressIndicatorView - 多种动画进度指示器
SVGView - SVG 解析器
LiquidSwipe - 液体导航动画