List 视图是显示滚动数据行的好方法,但有时还需要数据列,能够在大屏上显示更多数据。这时就要用到 LazyHGrid 和 LazyVGrid
LazyHGrid 用于显示水平数据, LazyVGrid 用于显示垂直数据LazyHGrid 和 LazyVGridLazyHGrid 、LazyVGrid 支持 iOS 13 之后的系统版本定义网格布局有以下几种常用方法:
// 1.创建了3列网格,并让每一列具备精确宽度
let columns = [
GridItem(.fixed(80)),
GridItem(.fixed(80)),
GridItem(.fixed(80))
]
// 2.控制每个单元格最小宽度,在此基础上网格在每行中容纳尽可能多的项目,所以列数不定,只写一个即可
let columns = [
GridItem(.adaptive(minimum: 80))
]
// 3.使用 .flexible 精确控制列数(4列,那里面的单元格大小可以自适应)
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
// 还可以设置间隔
let columns = [
GridItem(.flexible(), spacing: 15),
GridItem(.flexible())
]
// 4.结合起来使用:这将使第一列的宽度恰好为 100,第二列则会填充所有剩余空间
let columns = [
GridItem(.fixed(100)),
GridItem(.flexible()),
]
<aside>
💡 .flexible 用于自动调整列的宽度的大小,而 .adaptive 用于根据可用空间动态调整列的数量。根据具体的布局需求,你可以选择使用这两个选项中的一个来定义网格布局中的列。
</aside>


定义好布局后,应该将网格以及所需数量的项目放置在 ScrollView 内。您在网格中创建的每个项目都会自动分配一列,就像列表中的行自动放置在其父项中一样。我们只定义 LazyHGrid 和 LazyVGrid 两者中的一个,具体取决于想要哪种类型的网格。
// 定义垂直网格 LazyVGrid 时,参数是 columns
LazyVGrid(columns: layout, spacing: 20) {
ForEach(0..<100) {
Text("Item \\($0)")
}
}
// 定义水平网格 LazyHGrid 时,参数是 rows
LazyHGrid(rows: layout, alignment: .center) {
ForEach(0..<100) {
Text("Item \\($0)")
}
}
ScrollView {
LazyVGrid(columns: layout) {
ForEach(0..<1000) {
Text("Item \\($0)")
}
}
}
制作水平网格的过程几乎相同,您只需使 ScrollView 水平工作,然后使用行而不是列创建 LazyHGrid :
ScrollView(.horizontal) {
LazyHGrid(rows: layout) {
ForEach(0..<1000) {
Text("Item \\($0)")
}
}
}
Grid 视图可以创建静态视图网格,并精确控制每行和每列的内容。您可以使用 GridRow 标记各个行,然后还可以配置每个单元格的宽度。
// 作为基本示例,这将创建一个 2x2 网格,其中的文本反映每个单元格的位置:
Grid {
GridRow {
Text("Top Leading")
.background(.red)
Text("Top Trailing")
.background(.orange)
}
GridRow {
Text("Bottom Leading")
.background(.green)
Text("Bottom Trailing")
.background(.blue)
}
}
如果您不想每行具有相同数量的单元格,有3种选择:
// 第1种方法:什么都不做,SwiftUI 将自动插入空单元格以确保 rows 相等
struct ContentView: View {
@State private var redScore = 0
@State private var blueScore = 0
var body: some View {
Grid {
GridRow {
Text("Red")
ForEach(0..<redScore, id: \\.self) { _ in
Rectangle()
.fill(.red)
.frame(width: 20, height: 20)
}
}
GridRow {
Text("Blue")
ForEach(0..<blueScore, id: \\.self) { _ in
Rectangle()
.fill(.blue)
.frame(width: 20, height: 20)
}
}
}
.font(.title)
Button("Add to Red") { redScore += 1 }
Button("Add to Blue") { blueScore += 1 }
}
}
// 第2个方法:是将视图放入网格中,而不将它们包装在 GridRow 中,这将导致它们自己占据整行。这对于 Divider 视图非常有用
// 第3个方法:是使用 gridCellColumns() 修饰符,使一个单元格跨越多个列
Grid {
GridRow {
Text("Food")
Text("$200")
}
GridRow {
Text("Rent")
Text("$800")
}
Divider()
GridRow {
Text("$4600")
.gridCellColumns(2)
.multilineTextAlignment(.trailing)
}
}
.font(.title)
用 swiftUI 实现一个两列的网格视图,每个单元格的宽度是自适应的,也就是说排除额外的 padding 后,单元格的宽度应该是占屏幕一半。其次每个单元格的高度是固定的,例如 180 。然后每个单元格里面会展示一个尺寸不确定的 Image,希望 Image 是填充满整个单元格区域,同时等比例缩放的,溢出部分就裁切掉。
核心思路很清晰:
LazyVGrid + 两个 GridItem(.flexible())GridItem(.flexible()) 会让两列平分可用宽度,不需要手动计算.frame(maxWidth: .infinity, maxHeight: 180) 让图片横向撑满单元格,并固定高度上限.scaledToFill() 等比例放大填满;.clipped() 裁掉溢出部分.clipped() 必须在 .frame() 之后,否则裁切参考的边界不对// MARK: - 单元格视图代码示例
struct PhotoGridCell: View {
let imageName: String
var body: some View {
Image(imageName)
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: 180)
.clipped()
.cornerRadius(10)
}
}
但是这个代码发现不管怎么改,横版的图片都会撑破单元格,并且溢出的部分没有裁掉。
这是 SwiftUI 布局系统的一个经典陷阱。问题出在修饰符的执行顺序上:scaledToFill() 会先让图片按比例放大,然后向父级上报它"理想"的尺寸(对横版图来说宽度很大),LazyVGrid 接收到这个尺寸后就把单元格撑开了,.clipped() 只裁剪了视觉渲染,但没有阻止布局扩张。
解决方案:让一个固定尺寸的容器先占据布局空间,再用 overlay 把图片填进去,这样布局系统看到的永远是容器的尺寸,而不是图片的理想尺寸。核心原理:
Rectangle() 先向布局系统"声明"好尺寸(宽度撑满、高度 180)overlay 里的图片不参与布局计算,只在容器边界内渲染.clipped() 裁掉 overlay 中溢出容器边界的部分Rectangle 决定struct PhotoGridCell: View {
let imageName: String
var body: some View {
Rectangle()
.fill(Color.clear)
.frame(maxWidth: .infinity, maxHeight: 180)
.overlay {
Image(imageName)
.resizable()
.scaledToFill()
}
.clipped()
.cornerRadius(10)
}
}
好问题,这涉及 SwiftUI 布局系统一个很核心的概念区别。SwiftUI 布局每次都经历这几步:1. 基本原理
1. 父视图 → 向子视图"提议"一个尺寸(Propose)
2. 子视图 → 向父视图"上报"自己需要的尺寸(Report back)
3. 父视图 → 决定把子视图放在哪里
父视图最终的尺寸,取决于子视图上报的尺寸,而不是父视图自己的提议。
max 参数 vs 固定值,行为完全不同。这是你困惑的核心。frame 有两种用法,语义截然不同:
固定尺寸:这个 frame 会忽略子视图上报的尺寸,强制向父级声明"我就是 100×180"。子视图内容再大,父级看到的永远是 100×180。
.frame(width: 100, height: 180)
弹性尺寸:maxWidth: .infinity 的意思是"我可以扩展到无限宽",它是一个上限约束,不是一个固定值。这个 frame 会把子视图上报的尺寸透传给父级(在约束范围内)。
.frame(maxWidth: .infinity, maxHeight: 180)
之前代码实际发生了什么?
高度方向没问题,因为 maxHeight: 180 把 180 以上的高度截住了。但宽度方向,maxWidth: .infinity 对 480 毫无约束,LazyVGrid 收到"我需要 480 宽",于是把列撑开了。
Image(imageName)
.resizable()
.scaledToFill() // ← 横版图:上报宽度 = 480(举例)
.frame(maxWidth: .infinity, maxHeight: 180) // ← maxWidth 是无限,480 没超限,原样透传
.clipped() // ← 只管视觉裁切,不影响布局尺寸
overlay 方案为什么有效?
overlay 里的内容是脱离布局流的——它只做渲染,不向父级上报尺寸。所以 LazyVGrid 收到的尺寸始终来自 Rectangle,图片的理想宽度对它来说是透明的。
Rectangle()
.frame(maxWidth: .infinity, maxHeight: 180) // ← Rectangle 本身没有内容尺寸,它就老老实实按列宽上报
.overlay {
Image(...)
.resizable()
.scaledToFill() // ← 图片在这里再大,也不参与布局计算
}
.clipped()
<aside> 💡
</aside>