Grid 视图,灵感来自 CSS Grid,使用 SwiftUI 编写
Grid 是一种在 SwiftUI 中布局视图的强大而简便的方法
请查看下面的完整文档。
Grid 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile 中
pod 'ExyteGrid'
Grid 可通过 Swift Package Manager 获得。
将其作为包依赖项添加到现有的 Xcode 项目
git clone git@github.com:exyte/Grid.git
cd Grid/Example/
pod install
open Example.xcworkspace/
您可以通过不同的方式实例化 Grid
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)
}
Grid(colorModels, id: \.self, tracks: 3) {
ColorView($0)
}
在 ViewBuilder 中,您还可以使用常规的 ForEach
语句。无法从初始化的 ForEach 视图中获取 KeyPath id 值。在执行动画时,其内部内容将通过视图顺序来区分。最好将 ForEach
与 Identifiable
模型或 GridGroup 一起使用,GridGroup 可以使用显式 ID 值或 Identifiable
模型创建,以便跟踪网格视图及其在动画中的 View
表示。
Grid(tracks: 4) {
ColorView(.red)
ColorView(.purple)
ForEach(0..<4) { _ in
ColorView(.black)
}
ColorView(.orange)
ColorView(.green)
}
ViewBuilder
闭包中的视图数量限制为 10。无法从常规 SwiftUI Group
视图中获取内容视图。要超过此限制,您可以使用 GridGroup
。GridGroup
中的每个视图都作为单独的网格项放置。与 Group
视图不同,对 GridView
的任何外部方法修改都不会应用于子视图。因此它只是一个可枚举的容器。此外,GridGroup
可以通过 Range<Int>
、Identifiable
模型,或者通过显式指定的 ID 创建。
您还可以使用 GridGroup
将视图的标识绑定到给定的单个 Hashable
或 Identifiable
值。这将生成具有相同标识的新视图的过渡动画。
无法使用 View 的 .id()
修饰符,因为内部 ForEach
视图会清除该值
您可以使用 GridGroup.empty
来定义内容的缺失。
示例
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)
}
}
您可以混合使用 3 种类型的轨道尺寸
.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 是一个分数单位,.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)
}
此外,您可以仅指定一个 Int
字面量作为轨道尺寸。它等于重复 .fr(1)
轨道尺寸
Grid(tracks: 3) { ... }
等于
Grid(tracks: [.fr(1), .fr(1), .fr(1)]) { ... }
当使用非灵活轨道尺寸时,要分配的额外空间可能大于网格项能够占用的空间。要填充该空间,您可以使用 .gridCellBackground(...)
和 gridCellOverlay(...)
修饰符。
每个网格视图都可以跨越提供的网格轨道数。您可以使用 .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)
}
}
对于每个视图,您都可以通过指定列、行或两者来设置显式起始位置。如果没有指定起始位置,则将自动定位视图。首先,放置具有列和行起始位置的视图。其次,自动放置算法尝试放置具有列或行起始位置的视图。如果存在任何冲突,则会自动放置此类视图,并且您会在控制台中看到警告。最后,放置没有显式起始位置的视图。
使用 .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)
}
Grid 有 2 种类型的轨道。第一种是您指定 轨道尺寸 的轨道 - 固定轨道。固定意味着轨道的数量是已知的。第二种是与固定轨道正交的增长轨道类型:您的内容在哪里增长。Grid 流向定义了项目增长的方向
默认。 列数是固定的,并且 定义为轨道尺寸。网格项在列之间移动并切换到最后一列之后的下一行时放置。行数正在增长。
行数是固定的,并且 定义为轨道尺寸。网格项在行之间移动并切换到最后一行之后的下一列时放置。列数正在增长。
可以在 grid 构造函数中以及使用 .gridFlow(...)
grid 修饰符指定 Grid 流向。第一个选项具有更高的优先级。
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)
}
}
}
有两种内容模式
在此模式下,内部网格内容能够滚动到 增长方向。与网格流向(增长)正交的网格轨道被隐式地假定为具有 .fit 尺寸。这意味着它们的尺寸必须在各自的维度中定义。
可以在 grid 构造函数中以及使用 .gridContentMode(...)
grid 修饰符指定 Grid 内容模式。第一个选项具有更高的优先级。
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)
}
}
自动放置算法可以坚持以下两种策略之一
默认。 放置算法仅在放置项目时在网格中“向前”移动,永远不会回溯以填充孔。这确保了所有自动放置的项目都“按顺序”显示,即使这会留下稍后项目可以填充的孔。
尝试填充网格中较早的孔,如果稍后出现较小的项目。当这样做会填充较大项目留下的孔时,这可能会导致项目看起来无序。
可以在 grid 构造函数中以及使用 .gridPacking(...)
grid 修饰符指定 Grid 填充。第一个选项具有更高的优先级。
示例
@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)
}
}
有几种方法可以定义轨道之间的水平和垂直间距
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)
}
}
使用此选项可为特定的单个网格项指定对齐方式。它比 gridCommonItemsAlignment
具有更高的优先级
以 gridItemAlignment
的方式应用于每个项目,但不覆盖其各自的 gridItemAlignment
值。
应用于整个网格内容。当内容大小小于网格的可用空间时生效。
示例
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)
}
}
}
你可以使用 .gridAnimation()
网格修饰符来定义一个特定的动画,该动画将被应用到内部的 ZStack
。
默认情况下,网格中的每个视图都与其后续索引关联作为其 ID。 因此,SwiftUI 依赖于初始和最终状态下的网格视图位置来执行动画过渡。你可以使用由 Identifiable
模型初始化的 ForEach 或 GridGroup,或者显式 KeyPath 作为 ID,将特定的 ID 与网格视图关联,以强制动画以正确的方式执行。
无法从初始化的 ForEach 视图获取 KeyPath id 值。 其内部内容将在进行动画时通过视图顺序来区分。最好使用带有 Identifiable
模型的 ForEach 或使用显式 ID 值或 Identifiable
模型创建的 GridGroup,以便跟踪网格视图及其在动画中的 View
表示。
可以缓存 Grid 的生命周期内的网格布局。
仅支持 iOS
网格缓存可以在网格构造函数中指定,也可以使用 .gridCaching(...)
网格修饰符指定。 第一种选择具有更高的优先级。
默认。 缓存是利用 NSCache 实现的。 它将在内存警告通知时清除所有缓存的布局。
不使用缓存。 布局计算将在 Grid 生命周期的每一步执行。
从 Swift 5.3 开始,我们可以使用自定义函数构建器,而没有任何问题。 这给了我们
完全支持 if/if else
, if let/if let else
, switch
语句在 Grid
和 GridGroup
主体中。
一种从嵌套的 GridGroup
和 ForEach
传递视图 ID 的更好方法
使用 @GridBuilder
属性和 some View
返回类型,可以从函数和变量返回异构视图
@GridBuilder
func headerSegment(flag: Bool) -> some View {
if flag {
return GridGroup { ... }
else {
return ColorView(.black)
}
}
问题:如果 GridBuilder 中的任何内容项使用任何外部数据,则 Grid 不会更新它。 例如
@State var titleText: String = "title"
Grid(tracks: 2) {
Text(titleText)
Text("hello")
}
即使 titleText 发生更改,Grid 也没有更新它。
gridItemAlignment
修饰符,用于对齐每个项目gridCommonItemsAlignment
修饰符,用于对齐所有项目gridContentAlignment
修饰符,用于对齐整个网格内容gridAlignment
修饰符,用于对齐每个项目gridCommonItemsAlignment
修饰符,用于对齐 Grid 中的所有项目@GridBuilder
函数构建器Identifiable
或 Hashable
值初始化的 GridGroup
Grid 在 MIT 许可证下可用。 有关更多信息,请参见 LICENSE 文件。
PopupView - Toasts 和 popups 库
ScalingHeaderScrollView - 一个带有粘性标题的滚动视图,该标题会在你滚动时缩小
AnimatedTabBar - 带有许多预设动画的标签栏
MediaPicker - 可自定义的媒体选择器
Chat - 聊天 UI 框架,具有完全可自定义的消息单元格、输入视图和内置的媒体选择器
OpenAI - OpenAI REST API 的 Wrapper 库
AnimatedGradient - 动画线性渐变
ConcentricOnboarding - 动画入职流程
FloatingButton - 浮动按钮菜单
ActivityIndicatorView - 多个动画加载指示器
ProgressIndicatorView - 多个动画进度指示器
FlagAndCountryCode - 每个国家的电话代码和标志
SVGView - SVG 解析器
LiquidSwipe - 流体导航动画