GeometryReader

Geometry 是几何学的意思,它和大小与位置有关。GeometryReader 是最强大的布局视图之一,它可以在运行时读取视图的大小和位置,并在这些值随时间变化时继续读取它们


1. 扩张型视图

使用 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)
}

2. 坐标空间 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 个点
*/

3. 获取元素尺寸

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)
}

4. 获取安全区域保留的空间值

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)")
}

Screenshot - 2023-09-18 14.52.24.png


onGeometryChange

该修饰符仅在 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个值:

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

应用:结合 ScrollView

当使用 GeometryProxyframe(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)
}