介绍
早在 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 等)。请耐心等待,这将在以后有所帮助。
有两种类型的容器视图。我认为这些类型没有正式名称。我只会称它们为“有布局的容器”和“没有布局的容器”。用几个例子可以更好地解释这一点:
1 | struct ContentView: View { |
同样可以这么写:
1 | struct ContentView: View { |
从示例中可以看出,Group 修饰符分别应用于每个包含的视图。此外,Group 视图本身没有提供任何布局,也没有任何自己的几何图形。所有布局都由其父级执行:HStack。
但是,具有布局的容器(例如 HStack)上的修饰符应用于容器,该容器确实具有自己的几何形状:
1 | struct ContentView: View { |
您可能会问,当 Group 没有父级时会发生什么。这不是问题。当没有布局容器存在时,SwiftUI 会隐式使用 VStack。这就是为什么这也有效:
1 | struct ContentView: View { |
另一个没有布局的容器示例是 ForEach
:
1 | struct ContentView: View { |
这与网格有什么关系?我们将在下一节中找到答案。
我们的第一个网格
让我们建立我们的第一个网格。语法非常简单。您使用 Grid 容器视图,然后通过对 GridRow 容器内的单元格视图进行分组来定义其行。
1 | struct ContentView: View { |
这就是我们谈论容器的地方。如果我告诉你 Grid 是一个带有布局的容器,但 GridRow 不是。这意味着我们可以重写我们的代码并获得相同的结果:
1 | struct ContentView: View { |
请注意,并非所有行都具有相同数量的单元格。尽管这里的大多数示例都可以,但每一行可以包含任意数量的单元格。
探索网格选项
在以下部分中,我们将探讨不同的网格大小、对齐和跨越选项。但为了让事情变得更容易,我创建了一个名为 Grid Trainer 的小应用程序。该应用程序可让您以交互方式使用所有这些网格参数。当您更改网格时,该应用程序还将向您显示生成您创建的网格的代码。
整个应用程序位于一个 swift 文件中,因此只需几秒钟即可完成设置。只需创建一个新的 Xcode 项目,将 ContentView.swift 文件替换为此 gist 文件中的文件,就可以开始了。请注意,虽然我在设计应用程序时主要考虑了 macOS,但该应用程序在 iPad 上也能流畅运行。无需更改。
当您阅读以下部分时,最好运行 Grid Trainer 应用程序并测试您对网格的理解。试着看看你是否可以预测当你改变参数时网格会做什么。每次你得到你所期望的不同结果时,你都会学到一些关于网格的新东西。如果你得到你所期望的,你会重申你已经知道的。
空间
与 HStack 和 VStack 类似,Grid 容器具有用于间距的垂直和水平参数。如果未指定,则将使用系统默认值。
1 | Grid(horizontalSpacing: 5.0, verticalSpacing: 15.0) { |
列宽,行高
网格中的单元格是视图,视图会适应父级提供的大小。在这种情况下,父级是网格。通常,列与其中最宽的单元格一样宽。在下面的示例中,橙色列的宽度由第二行中最宽的单元格决定。身高也是如此。在示例中,第二行与行中最高的紫色单元格一样高。
未定义大小的单元
默认情况下,网格将为单元格提供尽可能多的空间。那么如果一个网格是由一个 Rectangle() 视图组成的,会发生什么呢?如您所知,没有框架修饰符的形状喜欢增长以填充父级提供的所有空间。在这种情况下,网格将增长以填充其父级提供的所有空间。
在下面的示例中,绿色单元格在其水平维度上不受限制,因此它使用了所有可用空间。网格尽可能地增长,绿色单元格填充空间。然而,蓝色单元格被框架修改器限制为 50.0 pt 宽度。虚线表示网格边界。
1 | struct ContentView: View { |
到目前为止,没有什么太令人惊讶的。这与我们从使用 HStack 容器的第一天起就看到的行为相同。但是,Grids 在这里为我们提供了一个选择。我们可以让单元格避免让网格增长以获得额外的空间。例如,对于水平维度,单元格只会增长到与其列中最宽的单元格一样多的空间。这样的单元格在确定列宽方面没有任何作用。这是通过应用于相关单元格的 gridCellUnsizedAxes() 修饰符来完成的。它接收一个 Axis.Set 值。它可以是 .horizontal、.vertical 或两者的组合:[.horizontal, .vertical]。这告诉网格给定单元格选择不要求额外空间的维度。
如果您还没有,现在是开始使用 Grid Trainer 应用程序并挑战您迄今为止的知识的好时机。
在下面的示例中,红色单元格在水平轴上未调整大小,使其仅与绿色单元格一样大。即使父母提供更多,红细胞也不会接受。
1 | Grid { |
对齐路线
网格对齐
当单元格的视图小于可用空间时,对齐方式将取决于几个参数。第一个要考虑的参数是 Grid(alignment: Alignment)。它影响网格中的所有单元格,除非被下一个参数之一覆盖。如果未指定,则默认为 .center。
1 | Grid(alignment: .topLeading) { |
行垂直对齐
您还可以使用 GridRow(alignment: VerticalAlignment) 指定行对齐方式。请注意,在这种情况下,对齐方式只是垂直的。此行中的单元格将结合 Grid 参数和 GridRow 参数。行的垂直对齐将优先于对齐的网格垂直组件。在下面的示例中,具有 .topTrailing 值的网格与 .bottom 垂直行值相结合,会导致第二行中的单元格以 .bottomTrailing 对齐。其他行将使用网格对齐方式(即 .topTrailing)。
1 | Grid(alignment: .topTrailing) { |
列水平对齐
除了指定垂直行对齐方式外,您还可以指定列水平对齐方式。与行对齐的情况一样,该值将与行垂直值和网格的对齐值合并。您使用修饰符 gridColumnAlignment() 指示列的对齐方式
注意:文档非常清楚。 gridColumnAlignment 只能在每列一个单元格中使用。否则行为未定义。
在以下示例中,您可以看到所有对齐组合:
单元格 (1,1):对齐顶部前导。 (网格对齐)
单元格 (1, 2):对齐的 topTrailing。 (网格对齐+列对齐)
单元格(2,1):对齐的底部前导(网格对齐+行对齐)
单元格 (2,2):对齐的底部尾随(网格对齐 + 行对齐 + 列对齐)
1 | struct ContentView: View { |
单元格对齐
最后,您还可以使用 .gridCellAnchor(_: anchor: UnitPoint) 修饰符为单元格指定单独的对齐方式。此对齐方式将覆盖给定单元格的任何网格、列和行对齐方式。注意参数类型不是Alignment,而是UnitPoint。这意味着除了使用预定义的点 .topLeading、.center 等之外,您还可以创建任意点,例如 UnitPoint(x: 0.25, y: 0.75):
1 | Grid(alignment: .topTrailing) { |
文本基线对齐
除了常见的对齐方式,请记住您还可以使用文本基线对齐方式。对于 Grid 和 GridRow:
1 | Grid(alignment: .centerFirstTextBaseline) { |
没有 GridRow 的行
如果 Grid 在 GridRow 容器之外有一个视图,则它被用作跨越所有列的单个单元格行。这种类型的单元格的常见用途是创建分隔符。例如,您可以使用 Divider() 视图,或者更复杂的视图,如下例所示。请注意,我们通常不希望分隔线使网格增长到最大值,因此我们使视图在水平轴上未调整大小。这将使分隔线与最宽的行一样宽,但不会更宽。
1 | Grid(horizontalSpacing: 5.0, verticalSpacing: 5.0) { |
列跨越
Eager Grids
优于Lazy Grids
的优点之一是所有单元几何形状始终是已知的。这使得有一个跨越多列的单元格成为可能。要将单元格配置为跨越,请使用 .gridCellColumns(_ count: Int)
1 | Grid { |
注意歧义
考虑以下示例。我们每行有 4 个单元格。除了第一行的第二个单元格和第二行的第三个单元格之外,每个单元格都是 50.0 pt 宽。这些将尽可能地增长(不扩大网格)。这两个单元格也分别跨越两列。
1 | struct ContentView: View { |
你认为应该发生什么?如果仔细看,这是“先有鸡还是先有蛋的问题”。如果您查看第一行中的第二个单元格,它应该跨越到以下列。但是第二行中的以下列应该扩展到第三列。那是什么?我们可以满足一个条件或另一个条件,但不能同时满足这两个条件。这是因为第一行查看第二行以确定下一列,而第二行查看第一行以执行相同操作。 SwiftUI 需要以某种方式解决这个问题,如果你运行代码,你会得到以下结果:
为了打破平局,一个简单的解决方案是添加第三行:
1 | GridRow { |
第三排打破平局,这就是它的样子:
如果您不需要第三行,则无论如何都可以添加一个,但高度为零。不过,您可能仍需要处理间距。幸运的是,这并不常见,但我会提到以防您遇到这种情况。
蜂窝再访
在文章 Impossible Grids 中,我们是否探索了Lazy Grid
,我写了一个示例,说明如何使用这些网格来呈现蜂窝中的单元格。创建这样的网格是测试网格可能的极限的好方法,所以我想我会重复这个练习,但这次使用Eager Grids
。
此gist file中提供了完整的工作网格。如果需要图片来测试代码,可以访问 https://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:如果使垂直间距等于水平间距,则单元格将均匀分布。
初始点
为了让你开始,这里有一些代码。首先,我们需要一些数据:
1 | struct Person { |
您还需要一个六边形:
1 | struct HexagonShape: Shape { |
最后,你开始设置网格:
1 | struct ContentView: View { |
概括
今年添加的 Grid 视图使用起来非常简单,并且添加到我们已经拥有的现有布局容器视图中。然而,今年还引入了一个新的布局协议,在将我们的视图放置在屏幕上时,它提供了更多的选择。我们将在以后的文章中对此进行探讨。同时,我希望您喜欢这篇文章和 Grid 教练应用程序。