2024-10-23 18:31:30
,某些文章具有时效性,若有错误或已失效,请在下方留言。HStack 和 VStack
⽔平和竖直堆栈在布局⼦视图时采⽤的⽅法相同,只是对应的轴不⼀样。以 HStack 为例。
HStack(spacing: 0) {
Color.cyan
Text("Hello, World!")
Color.teal
}
如果我们提供⼀个⾜够⼤的宽度,那么渲染会按照预期进⾏:“Hello, World!” ⽂本显示在中间,它的左侧显示蓝⾊,右侧显示绿⾊。但是随着宽度逐渐变⼩,会发⽣有趣的事情:虽然还有⾜够的空间在同⼀⾏内完整显示⽂本和两边的颜⾊,但是⽂本还是选择了换⾏。

导致这⼀⾏为的原因,和 HStack 向⼦视图分配可⽤宽度的算法有关:
- ⾸先,HStack 会确定它的⼦视图们的灵活性。两个颜⾊视图是⽆限灵活的,不管向它们提供什么样的尺⼨,它们都会欣然接受。但是 Text 的宽度会有上限,Text 的宽度可能介于 0 到它的理想宽度之间,但是绝对不可能超过理想宽度。
- HStack 根据⼦视图的灵活性从低到⾼进⾏排序。它会跟踪所有的剩余⼦视图和可⽤的剩余宽度。
- 只要还有剩余的⼦视图,HStack 就会把剩余的宽度除以⼦视图的数量,然后把结果作为建议宽度提供给这个⼦视图。
有以下的几种方法可以修改这种行为。
- 对 Text 使⽤
fixedSize()
修饰符 - 使⽤
.layoutPriority
修饰符给 Text 设定⼀个布局优先级。这会让 HStack ⾸先把全部剩余宽度提供给 Text,然后再将除去 Text 之外的剩余宽度提供给两个颜⾊。
ZStack
当使⽤ ZStack 时,它的所有⼦视图的 frame 将组合起来,进⾏并集 (union) 操作,并依此计算 ZStack ⾃身的尺⼨。
ZStack {
Color.teal
Text("Hello, World!")
}
当我们把这个视图作为⼀个新的 iOS app 的根视图时,颜⾊部分会拉伸到整个安全区域的完整尺⼨。这是因为 ZStack 接受到的建议尺⼨是整个安全区域,然后它会将同样的值再提供给它的每个⼦视图。
滚动视图
对于滚动视图 (scroll view),下⾯两者是不同的东⻄,必须对它们区分对待:
- 实际的滚动视图本身的布局⾏为。
- 滚动视图中的内容
滚动视图本身,在滚动轴的⽅向上会接受⽗视图所提供的建议尺⼨;在另⼀个轴上,则使⽤它的内容的尺⼨。⽐如,如果我们把⼀个垂直的滚动视图作为根视图的话,它的⾼度就是会整个安全区域,宽度则取决于滚动内容。
在滚动⽅向上,滚动视图本质上具有⽆限的空间,它的内容在这个轴上也可以⽆限增⻓。因此,滚动视图在它的滚动轴上将使⽤ nil
作为建议尺⼨,在另外的轴上则将滚动视图⾃身所收到的尺⼨不加修改地提供给内容视图。
ScrollView {
Image("logo")
.resizable()
.aspectRatio(.fit)
Text("This is a longer text")
}
默认情况下,当我们为滚动视图的内容设定多个视图时,不管滚动⽅向如何,这些⼦视图都会被放在⼀个隐式的 VStack
中。假设滚动视图接受到的建议尺⼨是 320⨉480。它接下来会把
320⨉nil 提供给它的两个⼦视图,然后将它们分别放到滚动容器中。滚动视图本身的⾼度将始终是收到的建议⾼度 (480)。
GeometryReader
⼏何读取器本身也算⼀个视图,它会始终接受被建议的尺⼨,并将这个尺⼨通过⼀个 GeometryProxy
报告给它的视图构建闭包,通过这个值我们就可以获取到⼏何读取器的尺⼨了。GeometryProxy
还可以让我们访问当前的安全区域的缩进值 (inset
) 以及视图在特定坐标空间中的 frame
值,它还可以⽤来解析锚点 (anchor
)。
GeometryReader { proxy in
Text(verbatim: "\(proxy.size)")
}
如果我们不想让⼏何读取器影响布局,有下⾯两种⽅法可以做到:
- 当我们把⼀个完全灵活的视图放到
GeometryReader
⾥,它就不会影响布局。⽐如对于滚动视图来说,它本身就会变成建议尺⼨的⼤⼩,我们可以把它放到GeometryReader
⾥来获取建议尺⼨。 - 当把⼏何读取器放到
background
或者overlay
修饰符中,它不会影响主视图的尺⼨。在background
或overlay
中,我们可以使⽤GeometryProxy
来读取和视图⼏何特性相关的不同的值。因为主要⼦视图的尺⼨会被当作建议尺⼨给到次要⼦视图 (也就是⼏何读取器),所以这种做法在测量视图的⼤⼩时⾮常有效。
List
List
视图本身会使⽤被建议的尺⼨,和 ScrollView
类似,它也会把⾃⼰的宽度和 nil
的⾼度提议给它的⼦视图。
LazyHStack 和 LazyVStack
懒加载的堆栈和那些⾮懒加载的普通堆栈在布局⾏为上来说是⼀样的,它们
最后的尺⼨都是所有⼦视图框架的并集。但是,懒加载的堆栈不会尝试在主要轴上为⼦视图进⾏可⽤空间的分配。
LazyVGrid 和 LazyHGrid
LazyVGrid
布局的第⼀步是根据⽹格收到的建议尺⼨的宽度,来决定⽹格中列的宽度。⽹格⽀持三种列的类型:固定列
、灵活列
(flexible column) 和⾃适应列
(adaptive column)。固定宽度的列⽆条件地使⽤指定的宽度,灵活列 (在它们的宽度上界和下界之间) 灵活可变,⽽⾃适应列实际上是包含了多个⼦列的容器。
⽹格⾸先从建议宽度中减去所有固定列的宽度。对于剩余的列,算法将把剩余宽度除以剩余的列数,然后按照顺序建议给每⼀列。灵活列将会使⽤它的 (最⼩和/或最⼤宽度) 来限制这个建议宽度。⾃适应列⽐较特殊:⽹格会通过将建议的列宽度除以为这个⾃适应列指定的最⼩宽度,从⽽尽可能多地容纳⾃适应列中的⼦列。之后,这些⼦列会被拉伸到指定的最⼤宽度,以填充剩余空间。
现在,列的宽度已经都被计算出来了,⽹格会把所有列的宽度加起来,再算上列之间的间距,把结果作为它⾃⼰的宽度。对于⾼度,⽹格会向它的⼦视图建议 columnWidth⨉nil 的尺⼨来计算⾏的⾼度,然后把所有的⾏⾼和⾏间距加起来,
作为⾃⼰的⾼度。
⽹格会执⾏两次列布局算法:第⼀次在布局阶段中进⾏,⽽之后在渲染阶段⼜执⾏⼀次。在布局阶段,⽹格使⽤它收到的建议宽度作为起始宽度进⾏计算。⽽在渲染阶段中,它将使⽤布局阶段的结果作为起始宽度,并再⼀次将该宽度分配给各个列。

虽然两列的最⼩宽度加起来也可以轻松适配到 200 的可⽤宽度中去,但是⽹格不仅渲染出了边界,它甚⾄都没能在 200 宽度的中⼼进⾏渲染。
我们从剩余宽度 200 开始,⾸先减去 10 的间隔,得到 190。对于第⼀列,我们将 190 的宽度除以剩余的列数 2,得到 95。因为第⼀列的最⼩宽度设定为了 60,所以 95 并不会被限制,于是剩下的宽度保持在 95。第⼆列使⽤这个 95 的剩余宽度,但它的最⼩宽度需要是 120,所以它使⽤ 120 作为宽度。
但是,这并不是我们看到的⽹格渲染的结果:渲染出的⽹格第⼀列宽度是 108,第⼆列宽度 120,⽽我们计算得到的应该是 95 和 120。这就是第⼆遍布局过程发挥作⽤的地⽅。
在第⼀遍布局中,计算得到的⽹格整体宽度是 95 + 10 + 120 = 225。具有 200 固定框架的 frame 将把⽹格放置在正中,所以它会向左边偏移 (225 – 200) / 2 = 12.5 的距离。当⽹格需要渲染⾃⼰的时候,它会再次执⾏列布局算法,但这次的初始剩余宽度会从 225 开始。
第⼆遍布局从 225 开始,先减去 10 的间隔,剩下 215。于是第⼀列变成了 215 除以剩余列数 2,四舍五⼊后得到了 108。剩余的宽度是 215 减去 108,结果是 107。107 ⽐第⼆列设定的最⼩宽度 120 还⼩,所以该列将使⽤ 120 作为宽度。
除了令⼈感到惊讶的宽度外,我们现在也能解释为什么⽹格的渲染偏离了固定 frame 中⼼的情况了:因为⽹格外的 frame 是按照⽹格的原始宽度 225 进⾏计算的,但是⽹格现在会将⾃⼰的整体宽度渲染为 108 + 10 + 120 = 238,所以⽹格呈现出偏离中⼼ 7 point 的现象。
Grid
在两个维度上都进⾏堆叠。在布局时,Grid
的⼦视图将按照灵活性排序。取决于建议尺⼨,既可以按照两个维度上的灵活性计算,也可按照⼀个维度的灵活性计算。之后,Grid
将可⽤空间在两个维度上分配给⼦视图。
ViewThatFits
当我们想要根据不同的建议尺⼨显示不同的视图时,可以使⽤ ViewThatFits
。它接受多个⼦视图,并显示第⼀个能适配建议尺⼨的⼦视图。ViewThatFits
的⼯作⽅式是向每个⼦视图建议 nil 尺⼨,来获取⼦视图的理想尺⼨,然后显示 (按照出现在代码中的) 第⼀个理想尺⼨能适配到建议尺⼨之中的⼦视图。如果所有⼦视图都⽆法适配,那么它选取最后的⼀个⼦视图。
渲染修饰符
SwiftUI 有⼀系列会影响视图渲染⽅式,但不影响布局本身的视图修饰符,⽐如 offset,rotationEffect,scaleEffect 等。我们可以将这些修饰符想象成类似于执⾏了 CGContext.translate,它们修改视图绘制的位置。然⽽,从布局系统的⻆度来看,视图依然位于它最初的位置。
暂无评论内容