携手创作,一起生长!这是我参与「日新方案 8 月更文挑战」的第28天,点击查看活动概况
介绍
早在 2020 年,咱们就具有了在 SwiftUI(LazyVGrid 和 LazyHGrid)中制作网格的新视图控件。两年后,咱们又取得了另一种在网格(Grid)中显现视图的视图控件。可是,这些新增功能十分不同,不仅在您运用它的办法上,而且在它内部的行为办法上。 2020 年的观念很懒惰。这些新人很热心。
lazy grids
不会烘托甚至实例化屏幕外的视图。单元格视图仅在它们被翻滚时创立,而且在它们翻滚时停止计算。
这篇文章的主题 Eager Grids 正好相反。 SwiftUI 不在乎它们是在屏幕上仍是在屏幕外。一切视图都被同等对待。这或许会出现很多单元的功能问题。可是,多少是一个很大的数字是一个不或许答复的问题。这将取决于您的单元格视图的杂乱性。
所以假如lazy grids
表现更好,这就引出了一个问题,我为什么要运用Eager Grids
?事实是,Eager Grids
比lazy grids
更有优势,反之亦然。例如,Eager Grids
支撑列跨过,而lazy grids
不支撑。归根结底,功能并不是唯一需求考虑的要素。在本文中,咱们将探究这些新网格,以便您在挑选其间一个时做出明智的决议。
关于容器视图的一句话
在咱们开端探究 Grid 视图之前,让我先谈谈容器视图。也便是说,接纳视图构建器并以特定办法呈现其内容的视图(HStack、VStack、ZStack、Lazy*Grid、Group、List、ForEach 等)。请耐心等待,这将在今后有所帮助。
有两种类型的容器视图。我以为这些类型没有正式名称。我只会称它们为“有布局的容器”和“没有布局的容器”。用几个比如能够更好地解说这一点:
struct ContentView: View {
var body: some View {
HStack {
Group {
Text("Hello")
Text("World")
Image(systemName: "network")
}
.padding(10)
.border(.red)
}
}
}
同样能够这么写:
struct ContentView: View {
var body: some View {
HStack {
Text("Hello")
.padding(10)
.border(.red)
Text("World")
.padding(10)
.border(.red)
Image(systemName: "network")
.padding(10)
.border(.red)
}
}
}
从示例中能够看出,Group 修饰符分别应用于每个包括的视图。此外,Group 视图自身没有供给任何布局,也没有任何自己的几许图形。一切布局都由其父级执行:HStack。
可是,具有布局的容器(例如 HStack)上的修饰符应用于容器,该容器确实具有自己的几许形状:
struct ContentView: View {
var body: some View {
HStack {
Text("Hello")
Text("World")
Image(systemName: "network")
}
.padding(10)
.border(.red)
}
}
您或许会问,当 Group 没有父级时会产生什么。这不是问题。当没有布局容器存在时,SwiftUI 会隐式运用 VStack。这便是为什么这也有效:
struct ContentView: View {
var body: some View {
Text("Hello")
Text("World")
Image(systemName: "network")
}
}
另一个没有布局的容器示例是 ForEach
:
struct ContentView: View {
var body: some View {
HStack {
ForEach(0..<5) { idx in
Text("\(idx)")
}
.padding(10)
.border(.blue)
}
}
}
这与网格有什么关系?咱们将鄙人一节中找到答案。
咱们的榜首个网格
让咱们树立咱们的榜首个网格。语法十分简略。您运用 Grid 容器视图,然后经过对 GridRow 容器内的单元格视图进行分组来界说其行。
struct ContentView: View {
var body: some View {
Grid {
GridRow {
Text("Cell #1")
.padding(20)
.border(.red)
Text("Cell #2")
.padding(20)
.border(.red)
}
GridRow {
Text("Cell #3")
.padding(20)
.border(.green)
Text("Cell #4")
.padding(20)
.border(.green)
}
}
.padding(10)
.border(.blue)
}
}
这便是咱们议论容器的当地。假如我告知你 Grid 是一个带有布局的容器,但 GridRow 不是。这意味着咱们能够重写咱们的代码并取得相同的成果:
struct ContentView: View {
var body: some View {
Grid {
GridRow {
Text("Cell #1")
Text("Cell #2")
}
.padding(20)
.border(.red)
GridRow {
Text("Cell #3")
Text("Cell #4")
}
.padding(20)
.border(.green)
}
.padding(10)
.border(.blue)
}
}
请留意,并非一切行都具有相同数量的单元格。虽然这儿的大多数示例都能够,但每一行能够包括恣意数量的单元格。
探究网格选项
在以下部分中,咱们将讨论不同的网格巨细、对齐和跨过选项。但为了让工作变得更简略,我创立了一个名为 Grid Trainer 的小应用程序。该应用程序可让您以交互办法运用一切这些网格参数。当您更改网格时,该应用程序还将向您显现生成您创立的网格的代码。
整个应用程序坐落一个 swift 文件中,因而只需几秒钟即可完成设置。只需创立一个新的 Xcode 项目,将 ContentView.swift 文件替换为此 gist 文件中的文件,就能够开端了。请留意,虽然我在规划应用程序时主要考虑了 macOS,但该应用程序在 iPad 上也能流畅运转。无需更改。
当您阅览以下部分时,最好运转 Grid Trainer 应用程序并测验您对网格的理解。试着看看你是否能够猜测当你改变参数时网格会做什么。每次你得到你所期望的不同成果时,你都会学到一些关于网格的新东西。假如你得到你所期望的,你会重申你现已知道的。
空间
与 HStack 和 VStack 相似,Grid 容器具有用于距离的笔直和水平参数。假如未指定,则将运用体系默认值。
Grid(horizontalSpacing: 5.0, verticalSpacing: 15.0) {
GridRow {
Rectangle().fill(Color(white: 0.20).gradient)
Rectangle().fill(Color(white: 0.40).gradient)
Rectangle().fill(Color(white: 0.60).gradient)
Rectangle().fill(Color(white: 0.80).gradient)
}
.frame(width: 50.0, height: 50.0)
GridRow {
Rectangle().fill(Color(white: 0.80).gradient)
Rectangle().fill(Color(white: 0.60).gradient)
Rectangle().fill(Color(white: 0.40).gradient)
Rectangle().fill(Color(white: 0.20).gradient)
}
.frame(width: 50.0, height: 50.0)
}
列宽,行高
网格中的单元格是视图,视图会习惯父级供给的巨细。在这种情况下,父级是网格。通常,列与其间最宽的单元格相同宽。鄙人面的示例中,橙色列的宽度由第二行中最宽的单元格决议。身高也是如此。在示例中,第二行与行中最高的紫色单元格相同高。
未界说巨细的单元
默认情况下,网格将为单元格供给尽或许多的空间。那么假如一个网格是由一个 Rectangle() 视图组成的,会产生什么呢?如您所知,没有结构修饰符的形状喜欢增加以填充父级供给的一切空间。在这种情况下,网格将增加以填充其父级供给的一切空间。
鄙人面的示例中,绿色单元格在其水平维度上不受限制,因而它运用了一切可用空间。网格尽或许地增加,绿色单元格填充空间。可是,蓝色单元格被结构修改器限制为 50.0 pt 宽度。虚线表示网格边界。
struct ContentView: View {
let dash = StrokeStyle(lineWidth: 1.0, lineCap: .round, lineJoin: .miter, dash: [5, 5], dashPhase: 0)
var body: some View {
HStack(spacing: 0) {
Circle().fill(.yellow).frame(width: 30, height: 30)
Grid(horizontalSpacing: 0) {
GridRow {
RoundedRectangle(cornerRadius: 15.0)
.fill(.green.gradient)
.frame(height: 50)
RoundedRectangle(cornerRadius: 15.0)
.fill(.blue.gradient)
.frame(width: 50, height: 50)
}
}
.overlay { Rectangle().stroke(style: dash) }
Circle().fill(.yellow).frame(width: 30, height: 30)
}
}
}
到目前为止,没有什么太令人惊讶的。这与咱们从运用 HStack 容器的榜首天起就看到的行为相同。可是,Grids 在这儿为咱们供给了一个挑选。咱们能够让单元格防止让网格增加以取得额定的空间。例如,关于水平维度,单元格只会增加到与其列中最宽的单元格相同多的空间。这样的单元格在确认列宽方面没有任何作用。这是经过应用于相关单元格的 gridCellUnsizedAxes() 修饰符来完成的。它接纳一个 Axis.Set 值。它能够是 .horizontal、.vertical 或两者的组合:[.horizontal, .vertical]。这告知网格给定单元格挑选不要求额定空间的维度。
假如您还没有,现在是开端运用 Grid Trainer 应用程序并挑战您迄今为止的常识的好时机。
鄙人面的示例中,赤色单元格在水平轴上未调整巨细,使其仅与绿色单元格相同大。即使爸爸妈妈供给更多,红细胞也不会接受。
Grid {
GridRow {
RoundedRectangle(cornerRadius: 5.0)
.fill(.green.gradient)
.frame(width: 160.0, height: 80.0)
RoundedRectangle(cornerRadius: 5.0)
.fill(.blue.gradient)
.frame(width: 80.0, height: 80.0)
}
GridRow {
RoundedRectangle(cornerRadius: 5.0)
.fill(.red.gradient)
.frame(height: 80.0)
.gridCellUnsizedAxes(.horizontal)
RoundedRectangle(cornerRadius: 5.0)
.fill(.yellow.gradient)
.frame(width: 80.0, height: 80.0)
}
}
对齐道路
网格对齐
当单元格的视图小于可用空间时,对齐办法将取决于几个参数。榜首个要考虑的参数是 Grid(alignment: Alignment)。它影响网格中的一切单元格,除非被下一个参数之一掩盖。假如未指定,则默以为 .center。
Grid(alignment: .topLeading) {
GridRow {
Rectangle().fill(.yellow.gradient)
.frame(width: 50.0, height: 50.0)
Rectangle().fill(.green.gradient)
.frame(width: 100.0, height: 100.0)
}
GridRow {
Rectangle().fill(.orange.gradient)
.frame(width: 100.0, height: 100.0)
Rectangle().fill(.red.gradient)
.frame(width: 50.0, height: 50.0)
}
}
行笔直对齐
您还能够运用 GridRow(alignment: VerticalAlignment) 指定行对齐办法。请留意,在这种情况下,对齐办法仅仅笔直的。此行中的单元格将结合 Grid 参数和 GridRow 参数。行的笔直对齐将优先于对齐的网格笔直组件。鄙人面的示例中,具有 .topTrailing 值的网格与 .bottom 笔直行值相结合,会导致第二行中的单元格以 .bottomTrailing 对齐。其他即将运用网格对齐办法(即 .topTrailing)。
Grid(alignment: .topTrailing) {
GridRow {
Rectangle().fill(Color(white: 0.25).gradient)
.frame(width: 120.0, height: 100.0)
Rectangle().fill(Color(white: 0.50).gradient)
.frame(width: 50.0, height: 50.0)
Rectangle().fill(Color(white: 0.50).gradient)
.frame(width: 120.0, height: 100.0)
}
GridRow(alignment: .bottom) {
Rectangle().fill(Color(white: 0.25).gradient)
.frame(width: 120.0, height: 100.0)
Rectangle().fill(Color(white: 0.50).gradient)
.frame(width: 50.0, height: 50.0)
Rectangle().fill(Color(white: 0.50).gradient)
.frame(width: 50.0, height: 50.0)
}
GridRow {
Rectangle().fill(Color(white: 0.25).gradient)
.frame(width: 120.0, height: 100.0)
Rectangle().fill(Color(white: 0.50).gradient)
.frame(width: 120.0, height: 100.0)
Rectangle().fill(Color(white: 0.50).gradient)
.frame(width: 50.0, height: 50.0)
}
}
列水平对齐
除了指定笔直行对齐办法外,您还能够指定列水平对齐办法。与行对齐的情况相同,该值将与行笔直值和网格的对齐值合并。您运用修饰符 gridColumnAlignment() 指示列的对齐办法
留意:文档十分清楚。 gridColumnAlignment 只能在每列一个单元格中运用。否则行为未界说。
在以下示例中,您能够看到一切对齐组合:
单元格 (1,1):对齐顶部前导。 (网格对齐) 单元格 (1, 2):对齐的 topTrailing。 (网格对齐+列对齐) 单元格(2,1):对齐的底部前导(网格对齐+行对齐) 单元格 (2,2):对齐的底部尾随(网格对齐 + 行对齐 + 列对齐)
struct ContentView: View {
var body: some View {
Grid(alignment: .topLeading, horizontalSpacing: 5.0, verticalSpacing: 5.0) {
GridRow {
CellView(color: .green, width: 80, height: 80)
CellView(color: .yellow, width: 80, height: 80)
.gridColumnAlignment(.trailing)
CellView(color: .orange, width: 80, height: 120)
}
GridRow(alignment: .bottom) {
CellView(color: .green, width: 80, height: 80)
CellView(color: .yellow, width: 80, height: 80)
CellView(color: .orange, width: 80, height: 120)
}
GridRow {
CellView(color: .green, width: 120, height: 80)
CellView(color: .yellow, width: 120, height: 80)
CellView(color: .orange, width: 80, height: 80)
}
}
}
struct CellView: View {
let color: Color
let width: CGFloat
let height: CGFloat
var body: some View {
RoundedRectangle(cornerRadius: 5.0)
.fill(color.gradient)
.frame(width: width, height: height)
}
}
}
单元格对齐
最后,您还能够运用 .gridCellAnchor(_: anchor: UnitPoint) 修饰符为单元格指定独自的对齐办法。此对齐办法将掩盖给定单元格的任何网格、列和行对齐办法。留意参数类型不是Alignment,而是UnitPoint。这意味着除了运用预界说的点 .topLeading、.center 等之外,您还能够创立恣意点,例如 UnitPoint(x: 0.25, y: 0.75):
Grid(alignment: .topTrailing) {
GridRow {
Rectangle().fill(.green.gradient)
.frame(width: 120.0, height: 100.0)
Rectangle().fill(.blue.gradient)
.frame(width: 50.0, height: 50.0)
.gridCellAnchor(UnitPoint(x: 0.25, y: 0.75))
}
GridRow {
Rectangle().fill(.blue.gradient)
.frame(width: 50.0, height: 50.0)
Rectangle().fill(.green.gradient)
.frame(width: 120.0, height: 100.0)
}
}
文本基线对齐
除了常见的对齐办法,请记住您还能够运用文本基线对齐办法。关于 Grid 和 GridRow:
Grid(alignment: .centerFirstTextBaseline) {
GridRow {
Text("Align")
Rectangle()
.fill(.green.gradient.opacity(0.7))
.frame(width: 50, height: 50)
}
}
.font(.system(size: 36))
没有 GridRow 的行
假如 Grid 在 GridRow 容器之外有一个视图,则它被用作跨过一切列的单个单元格行。这种类型的单元格的常见用处是创立分隔符。例如,您能够运用 Divider() 视图,或许更杂乱的视图,如下例所示。请留意,咱们通常不期望分隔线使网格增加到最大值,因而咱们使视图在水平轴上未调整巨细。这将使分隔线与最宽的行相同宽,但不会更宽。
Grid(horizontalSpacing: 5.0, verticalSpacing: 5.0) {
GridRow {
RoundedRectangle(cornerRadius: 5.0).fill(.green.gradient)
RoundedRectangle(cornerRadius: 5.0).fill(.purple.gradient)
RoundedRectangle(cornerRadius: 5.0).fill(.blue.gradient)
}
.frame(width: 50.0, height: 50.0)
Rectangle()
.fill(LinearGradient(colors: [.gray, .clear, .gray], startPoint: .leading, endPoint: .trailing))
.frame(height: 2.0)
.gridCellUnsizedAxes(.horizontal)
GridRow {
RoundedRectangle(cornerRadius: 5.0).fill(.green.gradient)
RoundedRectangle(cornerRadius: 5.0).fill(.purple.gradient)
RoundedRectangle(cornerRadius: 5.0).fill(.blue.gradient)
}
.frame(width: 50.0, height: 50.0)
}
列跨过
Eager Grids
优于Lazy Grids
的长处之一是一切单元几许形状始终是已知的。这使得有一个跨过多列的单元格成为或许。要将单元格配置为跨过,请运用 .gridCellColumns(_ count: Int)
Grid {
GridRow {
RoundedRectangle(cornerRadius: 5.0).fill(.green.gradient)
.frame(width: 50.0, height: 50.0)
RoundedRectangle(cornerRadius: 5.0).fill(.yellow.gradient)
.frame(height: 50.0)
.gridCellColumns(3)
.gridCellUnsizedAxes(.horizontal)
RoundedRectangle(cornerRadius: 5.0).fill(.purple.gradient)
.frame(width: 50.0, height: 50.0)
}
GridRow {
RoundedRectangle(cornerRadius: 5.0).fill(.green.gradient)
.frame(width: 50.0, height: 50.0)
RoundedRectangle(cornerRadius: 5.0).fill(.yellow.gradient)
.frame(width: 50.0, height: 50.0)
RoundedRectangle(cornerRadius: 5.0).fill(.orange.gradient)
.frame(width: 50.0, height: 50.0)
RoundedRectangle(cornerRadius: 5.0).fill(.red.gradient)
.frame(width: 50.0, height: 50.0)
RoundedRectangle(cornerRadius: 5.0).fill(.purple.gradient)
.frame(width: 50.0, height: 50.0)
}
}
留意歧义
考虑以下示例。咱们每行有 4 个单元格。除了榜首行的第二个单元格和第二行的第三个单元格之外,每个单元格都是 50.0 pt 宽。这些将尽或许地增加(不扩大网格)。这两个单元格也分别跨过两列。
struct ContentView: View {
var body: some View {
Grid(horizontalSpacing: 20.0, verticalSpacing: 20.0) {
GridRow {
CellView(width: 50.0, color: .green)
CellView(color: .purple)
.gridCellColumns(2)
CellView(width: 50.0, color: .blue)
CellView(width: 50.0, color: .yellow)
}
.gridCellUnsizedAxes([.horizontal, .vertical])
GridRow {
CellView(width: 50.0, color: .green)
CellView(width: 50.0, color: .purple)
CellView(color: .blue)
.gridCellColumns(2)
CellView(width: 50.0, color: .yellow)
}
.gridCellUnsizedAxes([.horizontal, .vertical])
}
}
struct CellView: View {
var width: CGFloat? = nil
let color: Color
var body: some View {
RoundedRectangle(cornerRadius: 5.0)
.fill(color.gradient)
.frame(width: width, height: 50.0)
}
}
}
你以为应该产生什么?假如仔细看,这是“先有鸡仍是先有蛋的问题”。假如您查看榜首行中的第二个单元格,它应该跨过到以下列。可是第二行中的以下列应该扩展到第三列。那是什么?咱们能够满足一个条件或另一个条件,但不能一起满足这两个条件。这是因为榜首行查看第二行以确认下一列,而第二行查看榜首行以执行相同操作。 SwiftUI 需求以某种办法解决这个问题,假如你运转代码,你会得到以下成果:
为了打破平局,一个简略的解决方案是添加第三行:
GridRow {
CellView(width: 50, color: .green)
CellView(width: 50, color: .purple)
CellView(width: 50, color: .blue)
CellView(width: 50, color: .yellow)
}
第三排打破平局,这便是它的姿态:
假如您不需求第三行,则无论怎么都能够添加一个,但高度为零。不过,您或许仍需求处理距离。走运的是,这并不常见,但我会提到以防您遇到这种情况。
蜂窝再访
在文章 Impossible Grids 中,咱们是否探究了Lazy Grid
,我写了一个示例,阐明怎么运用这些网格来呈现蜂窝中的单元格。创立这样的网格是测验网格或许的极限的好办法,所以我想我会重复这个操练,但这次运用Eager Grids
。
此gist file中供给了完整的工作网格。假如需求图片来测验代码,能够访问 this-person-does-not-exist.com。您能够下载带有随机面孔的不存在的人的方形图片!它们是人工智能生成的。 视频中运用的图片来自该网站。
从方形到六边形的过程
咱们必须从某个当地开端,所以咱们将创立一个方形图像网格,然后逐步添加代码将咱们的简略网格转换为蜂窝。
到现在为止,您应该具备完成转换所需的一切常识。我将为您供给一个起点和您需求执行的一系列过程,以便成功完成转换。可是,假如您没有时间,或许遇到困难,您能够查看上述 gist 文件中的代码。该代码有注释,指示它执行的每个过程的位置。
请留意,单元格的翻转并不是操练的一部分,但我也将其包括在要点中。
以下视频显现了起点以及它怎么变成蜂窝:
过程#1:咱们从方形图片网格开端。 过程#2:六边形没有 1:1 的尺度比。它的高度等于宽度 * cos(.pi/6)。假如您想知道原因,请查看 Impossible Grids,我在其间解说了原因。 过程#3:用供给的六边形取舍图像。 过程#4:将偶数行和奇数行移动到相对的两侧。偏移量是六边形宽度的一半 + 网格水平距离。 第 5 步:行需求堆叠,因而您需求将行高减少到四分之三 (3/4)。为什么是 3/4?,再次查看 Impossible Grids,我解说了原因。 第 6 步:要删除空白区域,请取舍网格边框(或将其放在 ScrollView 中,它会为您进行取舍)。 过程#7:假如使笔直距离等于水平距离,则单元格将均匀分布。
初始点
为了让你开端,这儿有一些代码。首先,咱们需求一些数据:
struct Person {
let name: String
let image: String
var color: Color = .accentColor
var flipped: Bool = false
}
class DataModel: ObservableObject {
static let people: [Person] = [
Person(name: "Peter", image: "image-1"),
Person(name: "Carlos", image: "image-2"),
Person(name: "Jennifer", image: "image-3"),
Person(name: "Paul", image: "image-4"),
Person(name: "Charlotte", image: "image-5"),
Person(name: "Thomas", image: "image-6"),
Person(name: "Sophia", image: "image-7"),
Person(name: "Isabella", image: "image-8"),
Person(name: "Ivan", image: "image-9"),
Person(name: "Laura", image: "image-10"),
Person(name: "Scott", image: "image-11"),
Person(name: "Henry", image: "image-12"),
Person(name: "Laura", image: "image-13"),
Person(name: "Abigail", image: "image-14"),
Person(name: "James", image: "image-15"),
Person(name: "Amelia", image: "image-16"),
]
static let colors: [Color] = [.yellow, .orange, .red, .purple, .blue, .pink, .green, .indigo]
@Published var rows: [[Person]] = DataModel.buildDemoCells()
var columns: Int { rows.first?.count ?? 0 }
var colCount: CGFloat { CGFloat(columns) }
var rowCount: CGFloat { CGFloat(rows.count) }
static func buildDemoCells() -> [[Person]] {
var array = [[Person]]()
// Add 7 rows
for r in 0..<7 {
var a = [Person]()
// Add 6 cells per row
for c in 0..<6 {
let idx = (r*6 + c)
var person = people[idx % people.count]
person.color = colors[idx % colors.count]
a.append(person)
}
array.append(a)
}
return array
}
}
您还需求一个六边形:
struct HexagonShape: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
let height = rect.height
let width = rect.height * cos(.pi/6)
let h = height / 4
let w = width / 2
let pt1 = CGPoint(x: rect.midX, y: rect.minY)
let pt2 = CGPoint(x: rect.midX + w, y: h + rect.minY)
let pt3 = CGPoint(x: rect.midX + w, y: h * 3 + rect.minY)
let pt4 = CGPoint(x: rect.midX, y: rect.maxY)
let pt5 = CGPoint(x: rect.midX - w, y: h * 3 + rect.minY)
let pt6 = CGPoint(x: rect.midX - w, y: h + rect.minY)
path.addLines([pt1, pt2, pt3, pt4, pt5, pt6])
path.closeSubpath()
}
}
}
最后,你开端设置网格:
struct ContentView: View {
@StateObject private var model = DataModel()
private let cellWidth: CGFloat = 100
private let cellHeight: CGFloat = 100
var body: some View {
VStack {
Grid(alignment: .center, horizontalSpacing: 2, verticalSpacing: 2) {
ForEach(model.rows.indices, id: \.self) { rowIdx in
GridRow {
ForEach(model.rows[rowIdx].indices, id: \.self) { personIdx in
let person = model.rows[rowIdx][personIdx]
Image(person.image)
.resizable()
.frame(width: cellWidth, height: cellHeight)
}
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
归纳
本年添加的 Grid 视图运用起来十分简略,而且添加到咱们现已具有的现有布局容器视图中。可是,本年还引入了一个新的布局协议,在将咱们的视图放置在屏幕上时,它供给了更多的挑选。咱们将在今后的文章中对此进行讨论。一起,我期望您喜欢这篇文章和 Grid 教练应用程序。