SwiftUI 布局: 了解 GeometryReader 内部
SwiftUI的 GeometryReader
允许我们根据其自身的大小和坐标来确定视图的大小和坐标,这是在SwiftUI中创建一些出色的效果的关键。
在使用GeometryReader
时,您应始终牢记SwiftUI的三步布局系统:父级为子级提供了一个尺寸,子级使用该尺寸确定自己的尺寸,父级使用该尺寸适当地定位子级。
在其最基本的用法中,GeometryReader
的作用是让我们读取父级提供的大小,然后使用该大小来操纵我们的视图。例如,我们可以使用GeometryReader
使文本视图具有所有可用空间的90%,而不管其内容是什么:
struct ContentView: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.frame(width: geo.size.width * 0.9)
.background(Color.red)
}
}
}
传入的那个geo
参数是一个GeometryProxy
,它包含能够提供的最大的尺寸,已应用的所有安全区域,以及一种用于读取Frame 值的方法。
GeometryReader
有一个有趣的副作用,可能一开始会吸引您:返回的视图具有灵活的首选大小,这意味着它将根据需要扩展以占用更多空间。如果将GeometryReader
放入VStack
中,然后在其下放一些其他文本,则可以看到它的作用,如下所示:
struct ContentView: View {
var body: some View {
VStack {
GeometryReader { geo in
Text("Hello, World!")
.frame(width: geo.size.width * 0.9, height: 40)
.background(Color.red)
}
Text("More text")
.background(Color.blue)
}
}
}
您会看到“More text”被直接推到屏幕底部,因为GeometryReader
占据了所有剩余空间。要查看实际效果,请将background(Color.green)
作为修改器添加到GeometryReader
中,您将看到它的大小。注意:这是首选大小,而不是绝对大小,这意味着它仍然可以灵活地取决于其父项。
在读取视图Frame时,GeometryProxy
提供了frame(in :)
方法,而不是简单的属性。这是因为“Frame”的概念包括X坐标和Y坐标,这些坐标孤立地没有任何意义–您是要视图的绝对X坐标还是Y坐标,还是与父视图相比它们的X坐标和Y坐标?
SwiftUI将这些选项称为坐标空间,特别是将这两个称为全局空间(相对于整个屏幕衡量我们的视图空间)和局部空间(相对于其父视图衡量我们的视图空间)。我们还可以通过将ordinateSpace()
修饰符附加到视图来创建自定义坐标空间,然后该视图的任何子项都可以读取相对于该坐标空间的Frame。
为了演示坐标空间是如何工作的,我们可以在不同的堆栈中创建一些示例视图,将自定义坐标空间附加到最外面的视图,然后在其中的一个视图中添加onTapGesture
,以便它可以全局/局部地打印Frame和使用自定义坐标空间。
尝试如下代码:
struct OuterView: View {
var body: some View {
VStack {
Text("Top")
InnerView()
.background(Color.green)
Text("Bottom")
}
}
}
struct InnerView: View {
var body: some View {
HStack {
Text("Left")
GeometryReader { geo in
Text("Center")
.background(Color.blue)
.onTapGesture {
print("Global center: \(geo.frame(in: .global).midX) x \(geo.frame(in: .global).midY)")
print("Custom center: \(geo.frame(in: .named("Custom")).midX) x \(geo.frame(in: .named("Custom")).midY)")
print("Local center: \(geo.frame(in: .local).midX) x \(geo.frame(in: .local).midY)")
}
}
.background(Color.orange)
Text("Right")
}
}
}
struct ContentView: View {
var body: some View {
OuterView()
.background(Color.red)
.coordinateSpace(name: "Custom")
}
}
该代码运行时所获得的输出取决于您所使用的设备,但这是我得到的:
- Global center: 202.0 x 455.16666666666663
- Custom center: 202.0 x 411.16666666666663
- Local center: 164.0 x 378.5
这些尺寸大部分是不同的,因此希望您能看到这些框架如何工作的全部内容:
- 全局中心X为202表示文本视图的中心距离屏幕左边缘202个点。这并不是定死在屏幕中央,因为“Left”和“Right”标签的尺寸不同。
- 全局中心Y为455.167表示文本视图的中心距离屏幕顶部边缘455.167点。这并不是定死在屏幕中央,因为顶部的安全区域比底部的安全区域大。
- 自定义中心X为202表示文本视图的中心距拥有“自定义”坐标空间的任何视图的左边缘202个点,在本例中为
OuterView
,因为我们将其附加到ContentView
中。此数字与全局位置匹配,因为OuterView
水平边缘挨到屏幕边缘。 - 自定义中心Y为411.167,表示文本视图的中心距离
OuterView
的顶部边缘为411.167点。该值小于全局中心Y,因为OuterView
不会延伸到安全区域。 - 局部中心X为164表示文本视图的中心距离其直接容器(在本例中为
GeometryReader
)的左边缘164个点。 - Y的本地中心378.5表示文本视图的中心距离其直接容器(也是
GeometryReader
)的顶部边缘378.6点。
您要使用哪个坐标空间取决于您要回答的问题:
- 是否想知道此视图在屏幕上的位置?使用全局空间。
- 是否想知道此视图相对于其父视图的位置?使用本地空间。
- 要知道该视图相对于其他视图的位置?使用自定义空间