Geometry 是几何学的意思,它和大小与位置有关。GeometryReader
是最强大的布局视图之一,它可以在运行时读取视图的大小和位置,并在这些值随时间变化时继续读取它们
GeometryReader
是容器视图,它创建时将获得 GeometryProxy
对象,可以用于存取父视图( parent view )的大小与位置等GeometryReader
还可以访问高度、宽度和安全区域插入等属性,这些属性可以帮助动态地设置视图的大小GeometryReader
允许使用它的大小和坐标来确定子视图的布局。在使用 GeometryReader
时,您应该始终牢记 SwiftUI 的3步布局系统:父级为子级提议一个大小;子级根据自身内容确定大小;父级再用它来适当地定位子级。 布局基础使用 GeometryReader
,它会自动扩展以占用布局中的可用空间,然后将其自己的内容与左上角对齐。
//GeometryReader 有一个有趣的作用:其返回的视图具有灵活的大小,它将根据需要扩展以占用更多空间。
//例如当将 GeometryReader 放入 VStack 中,然后在其下方添加更多文本,如下所示:
//可以看到 “More text” 被推到屏幕底部,因为 GeometryReader 扩张占据了所有可用空间。要查看实际效果可以将 background(.green) 修饰符添加到 GeometryReader 后面。注意:这是首选大小,而不是绝对大小,这意味着它可以根据其父级灵活调整,是动态的
VStack {
GeometryReader {
proxy in
Text("Hello, World!")
.frame(width: proxy.size.width * 0.9, height: 40)
.background(.red)
}
Text("More text")
.background(.blue)
}
GeometryProxy
提供了 frame(in:)
方法,其中的 in
参数的选项,被称为坐标空间,用于指定参照坐标系。主要坐标空间如下:
.global
:内置全局空间,它返回当前元素在整个屏幕上的帧信息。可以获取当前元素相对于屏幕左上角的坐标、大小和锚点….local
:内置本地空间,它返回当前元素在其父视图上的帧信息。可以获取当前元素相对于父视图左上角的坐标、大小和锚点….named(_:)
:自定义坐标空间,允许指定一个具有标识符(通常是String
类型)的坐标系。首先用 coordinateSpace(name:)
在需要的地方命名一个坐标系,然后使用 .frame(in: .named(""))
来获取相对于该坐标系的位置和尺寸….parent
:<aside> 💡 注意,GeometryReader 适应不同的设备尺寸时,最小值、中间值和最大值是变化的。
</aside>
struct OuterView: View {
var body: some View {
VStack {
Text("Top")
InnerView().background(.green)
Text("Bottom")
}
}
}
struct InnerView: View {
var body: some View {
HStack {
Text("Left")
GeometryReader { proxy in
Text("Center")
.background(.blue)
.onTapGesture {
// 1. 全局空间 frame(in: .global)
print("Global center: \\(proxy.frame(in: .global).midX) x \\(proxy.frame(in: .global).midY)")
// 2. 本地空间 frame(in: .local)
print("Local center: \\(proxy.frame(in: .local).midX) x \\(proxy.frame(in: .local).midY)")
//自定义空间 frame(in: .named(""))
print("Custom center: \\(proxy.frame(in: .named("Custom")).midX) x \\(proxy.frame(in: .named("Custom")).midY)")
}
}
.background(.orange)
Text("Right")
}
}
}
struct ContentView: View {
var body: some View {
OuterView()
.background(.red)
//声明一个自定义空间
.coordinateSpace(name: "Custom")
}
}
//最后打印出的尺寸大多不同,因此希望您能够了解这些框架如何工作的全部范围:
/*
1.【全局中心】 X 为 191 表示: 几何读取器的中心距屏幕左边缘 191 个点
2.【全局中心】 Y 为 440 表示: 几何读取器的中心距屏幕顶部边缘 440 点。这并不是死在屏幕中央,因为顶部比底部有更多的安全区域
3.【自定义中心】 X 为 191 表示: 几何读取器的中心距拥有“Custom”坐标空间的视图的左边缘 191 个点,在我们的示例中为 OuterView,因为我们将其附加在 ContentView 。该数字与全局位置匹配,因为 OuterView 水平地边到边地延伸。
4.【自定义中心】 Y 为 381 表示: 几何读取器的中心距 OuterView 顶部边缘381个点。该值小于全局中心Y,因为 OuterView 没有延伸到安全区域
5.【局部中心】 X 为 153 表示: 几何读取器的中心距其直接容器的左边缘 153 个点
6.【局部中心】 Y 为 350 表示: 几何读取器的中心距其直接容器的顶部边缘 350 个点
*/
GeometryReader
最基本的用法是读取父级建议的大小,然后使用它来操作视图。
GeometryReader
传入的 proxy 参数是 GeometryProxy,它被为引用几何体坐标空间的代理,它是 GeometryReader
大小和坐标空间的表示
// 例如,可以使用 GeometryReader 来设定视图的宽度:
GeometryReader {
proxy in
VStack(spacing: 10) {
Text("Width: \\(proxy.size.width)")
Text("Height: \\(proxy.size.height)")
Text("Hello, World!")
.offset(x: proxy.size.width)
.offset(y: proxy.size.height)
}
}
// 例如,可以使用 GeometryReader 来绝对定位视图:
GeometryReader { geometry in
Text("Upper Left")
.font(.title)
.position(x: geometry.size.width/5,
y: geometry.size.height/10)
Text("Lower Right")
.font(.title)
.position(x: geometry.size.width - 90,
y: geometry.size.height - 40)
}
VStack {
Text("geometry.safeAreaInsets.leading: \\(geometry.safeAreaInsets.leading)")
Text("geometry.safeAreaInsets.trailing: \\(geometry.safeAreaInsets.trailing)")
Text("geometry.safeAreaInsets.top: \\(geometry.safeAreaInsets.top)")
Text("geometry.safeAreaInsets.bottom: \\(geometry.safeAreaInsets.bottom)")
}
该修饰符仅在 iOS 18 可用。
onGeometryChange()
修饰符让我们可以跟踪视图的框架、大小或安全区域插图何时发生变化,然后根据结果采取任何操作。此外,此修改器还将报告您正在观看的内容的初始值,因此它对于一次性和连续监控都很有帮助。
// 例如,我们可以在第一次创建视图时以及每当视图发生更改时打印视图的 frame ,如下所示:
// frame 框架:是指视图的大小和位置,这意味着旋转设备或更改窗口大小可能会触发操作闭包
Text("Hello, world")
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { newValue in
print("Frame is now \\(newValue)")
}
分解这个修饰符,你会看到它接受3个值:
for:
。上面的代码中希望观察 frame,其类型就是 CGRect
;如果您只需要一个值,可以使用 Double
,或者如果你观察的对象可以归结为简单的真或假,那可能会使用Bool
GeometryProxy
对象,你可以使用它返回指定的任何值,例如 CGRect
,这就是后续关注变化的值newValue
对象,也就是前面转换闭包转换的值<aside> 💡 重要提示:虽然您可以使用操作闭包来设置视图状态,但请确保您不会意外陷入布局循环。例如,下面的代码会进入循环,因为它尝试在视图本身中显示视图的大小:
</aside>
struct ContentView: View {
@State private var textFrame = CGRect.zero
var body: some View {
Text("Size is: \\(textFrame)")
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { newValue in
textFrame = newValue
}
}
}
能够跟踪视图的大小和位置可以解锁多种功能,例如能够在其他位置创建具有匹配大小的视图。参照以下代码:
struct ContentView: View {
@State private var textFrame = CGRect.zero
@State private var textSize = 17.0
var body:some View {
VStack {
Text("Hello, world")
.font(.system(size: textSize))
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { newValue in
textFrame = newValue
}
Rectangle()
.frame(width: textFrame.width, height: textFrame.height)
Slider(value: $textSize,in: 10...30)
.padding()
}
}
}
当使用 GeometryProxy
的 frame(in:)
方法时,SwiftUI 将计算视图在要求的坐标空间中的当前位置。但随着视图移动,这些值将会发生变化,而 SwiftUI 可以自动确保 GeometryReader
保持更新。
使用 GeometryReader
,可以动态地从视图环境中获取值,将其绝对或相对位置,输入到各种修饰符中。更好的是甚至可以嵌套使用多个GeometryReader
,这样一个可以读取更高的视图,另一个可以读取更下方的视图。
//例如:可以通过在垂直滚动视图中创建 50 个文本视图来创建旋转螺旋效果
//每个视图都有无限的最大宽度,因此它们会占用所有屏幕空间,然后应用基于自身位置的 3D 旋转效果
struct ContentView: View {
let colors: [Color] = [.red, .green, .blue, .orange, .pink, .purple, .yellow]
var body: some View {
ScrollView {
ForEach(0..<50) {
index in
GeometryReader { proxy in
Text("Row #\\(index)")
.font(.title)
.frame(maxWidth: .infinity)
.background(colors[index % 7])
.rotation3DEffect(.degrees(proxy.frame(in: .global).minY / 5), axis: (x: 0, y: 1, z: 0))
//上面的意思是,读取Text在全局坐标中,最上方边缘的坐标,将其除以 5,得出旋转的值
//这样越靠下面的 Text,最上方边缘的坐标越大,于是旋转越多
}
.frame(height: 40)
}
}
}
}
//你会看到屏幕底部的文本视图被翻转,中间的文本视图旋转了大约 90 度,而最顶部的文本视图是正常的。
//重点是,当您滚动 ScrollView 时,里面的文本视图都会随着滚动而旋转,证明 geometryReader 里的 proxy 是会自动更新的
但这里有个问题,Text 视图只有在位于最顶部时才会达到其自然方向,这很难阅读。解决这个问题可以应用一个更复杂的 rotation3DEffect()
来减去主视图高度的一半,但这意味着需要使用第二个 GeometryReader
来获取主视图的大小。
struct ContentView: View {
let colors: [Color] = [.red, .green, .blue, .orange, .pink, .purple, .yellow]
var body: some View {
GeometryReader {
fullView in
ScrollView {
ForEach(0..<50) {
index in
GeometryReader {
proxy in
Text("Row #\\(index)")
.font(.title)
.frame(maxWidth: .infinity)
.background(colors[index % 7])
.rotation3DEffect(.degrees(proxy.frame(in: .global).minY - fullView.size.height / 2) / 5, axis: (x: 0, y: 1, z: 0))
//完成此操作后,视图将达到靠近屏幕中心的自然方向,看起来会更好。
}
.frame(height: 40)
}
}
}
}
}
ScrollView {
ZStack {
GeometryReader{
gr in
Image(systemName: "heart.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.offset(y: -gr.frame(in: .global).origin.y / 2)
}
VStack(spacing: 40) {
RoundedRectangle(cornerRadius: 20).frame(height: 200).opacity(0.7)
RoundedRectangle(cornerRadius: 20).frame(height: 200).opacity(0.7)
RoundedRectangle(cornerRadius: 20).frame(height: 200).opacity(0.7)
RoundedRectangle(cornerRadius: 20).frame(height: 200).opacity(0.7)
}
.padding(40)
}.edgesIgnoringSafeArea(.vertical)
}