GeometryReader

Geometry 是几何学的意思,它和大小与位置有关。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)
}

坐标空间 frame(in:)

GeometryProxy 提供了 frame(in:) 方法,其中的 in 参数的选项,被称为坐标空间,用于指定参照坐标系。主要坐标空间如下:

<aside> 💡 注意,GeometryReader 适应不同的设备尺寸时,最小值、中间值和最大值是变化的。

</aside>

Screenshot - 2023-09-18 14.48.25.png

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 个点
*/

Proxy 可获取的常用属性

在某坐标下的最小 x proxy.frame(in: ).minX
在某坐标下的中点 x proxy.frame(in: ).midX
在某坐标下的最大 x proxy.frame(in: ).maxX
在某坐标下的最小 y proxy.frame(in: ).minY
在某坐标下的中点 y proxy.frame(in: ).midY
在某坐标下的最大 y proxy.frame(in: ).maxY
获取 GeometryReader 视图的宽 proxy.size.width
获取 GeometryReader 视图的高 proxy.size.height
获取安全区域保留的空间值 proxy.safeAreaInsets.leading
获取安全区域保留的空间值 proxy.safeAreaInsets.trailing
获取安全区域保留的空间值 proxy.safeAreaInsets.top
获取安全区域保留的空间值 proxy.safeAreaInsets.bottom
在某坐标下的左上角原点的 x(经常等于minX) proxy.frame(in: ).origin.x
在某坐标下的左上角原点的 y(经常等于minY) proxy.frame(in: ).origin.y

Screenshot - 2025-02-13 11.51.23.png

Screenshot - 2025-02-13 12.01.37.png

Screenshot - 2025-02-13 11.51.36.png

Screenshot - 2025-02-13 11.53.38.png


onGeometryChange

该修饰符从 iOS 16.0 开始支持(对应 macOS 13.0、tvOS 16.0、watchOS 9.0)。

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个值:

  1. 想要观察的值的类型 for: 。上面的代码中希望观察 frame,其类型就是 CGRect ;如果您只需要一个值,可以使用 Double ,或者如果你观察的对象可以归结为简单的真或假,那可能会使用Bool
  2. 第 1 个转换闭包:它被赋予一个 GeometryProxy 对象,可以使用它返回指定类型的任何值,例如上面代码就是通过 proxy.frame(in: .global) 返回 CGRect,这就是后续关注变化的值
  3. 第 2 个动作闭包,当前面观察的值发生变化时,就会触发该动作闭包,它里面的 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()
        }
    }
}