.coordinateSpace(name:) 是 SwiftUI 中用于定义一个自定义坐标空间的修饰符。它为视图及其子视图指定一个命名坐标空间,之后可以通过 GeometryReader 或 GeometryProxy 来访问和使用这个命名坐标空间中的位置信息。
用法
基本语法
View()
.coordinateSpace(name: "Custom")
参数 name
是一个 Hashable 类型的标识符(通常是 String 或 UUID),用于标记该视图的命名坐标空间。
可以通过这个名字来引用该坐标空间。
坐标空间类型
SwiftUI 提供了三种主要的坐标空间:
1、.global
全局坐标空间,基于整个屏幕计算视图的位置。
2、.local
当前视图的局部坐标空间。
3、.named(“CustomSpace”)
自定义命名的坐标空间。
示例
假设有一个包含视图的布局,希望获取其中某个子视图在父视图中的位置。
struct ContentView: View {
var body: some View {
OuterView()
.background(.red)
.coordinateSpace(name: "Custom")
}
}
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 {
print("Global center: \(proxy.frame(in: .global).midX) x \(proxy.frame(in: .global).midY)")
print("Custom center: \(proxy.frame(in: .named("Custom")).midX) x \(proxy.frame(in: .named("Custom")).midY)")
print("Local center: \(proxy.frame(in: .local).midX) x \(proxy.frame(in: .local).midY)")
}
}
.background(.orange)
Text("Right")
}
}
}

输出的内容为:
Global center: 191.33333333333331 x 440.604248046875
Custom center: 191.33333333333331 x 381.604248046875
Local center: 153.66666666666666 x 350.6284993489583
根据iOS设备屏幕分辨率为3进行换算,可以得到三个尺寸:
Global的中心为:573.99 * 1321.8
Custom的中心为:573.99 * 1144.8
Local的中心为:460.98 * 1051.86
作图分析
通过设备截屏,可以在作图工具找到InnerView的中心位置:

1、Global的中心为:573.99 * 1321.8

在图片中可以看到黄色的区块尺寸为573.99 * 1321.8,这就是从Global中得到的坐标中心,但在作图时还是可以看到宽度有些偏差,可能是Global左侧并不会完整对齐或者是作图的偏差。但可以从这里大体看出proxy.frame(in: .global)是从屏幕的左上角到GeometryReader中心的像素距离。
2、Custom的中心为:573.99 * 1144.8

图片的紫色区域尺寸为573.99 * 1144.8,这是从自定义的Custom中计算的坐标,这里的紫色右侧也没有完整的与中心线对齐,这里是从OuterView()所占的区域左上角开始计算的。
需要注意的是,与Global相比,这里没有覆盖安全区域。proxy.frame(in: .name(“Custom”))从Custom所占区域的左上角计算到OuterView()中心点的距离:
print("Custom center: \(proxy.frame(in: .named("Custom")).midX) x \(proxy.frame(in: .named("Custom")).midY)")
3、Local的中心为:460.98 * 1051.86

最后是在Local的中心距离,是从内部视图左上角开始计算的。
同理,当把minX和minY从计算中心改为计算OuterView()右下角的坐标时,代码则需要改为:
print("Global center: \(proxy.frame(in: .global).maxX) x \(proxy.frame(in: .global).maxY)")
print("Custom center: \(proxy.frame(in: .named("Custom")).maxX) x \(proxy.frame(in: .named("Custom")).maxY)")
print("Local center: \(proxy.frame(in: .local).maxX) x \(proxy.frame(in: .local).maxY)")
这里的maxX和maxY分别是从前面的三个坐标左上角到OuterView()的右下角的坐标距离。

通过这里可以了解到coordinateSpace的坐标是从所在视图的左上角为原点计算所在视图的像素距离,全部可以盖住InnerView视图区域。
但作图时可以看到高度一致,但是宽度并没有到达屏幕的一半,可能是设备宽度也存在安全区域或者其他问题导致。
另一个示例
struct ContentView: View {
var body: some View {
VStack {
Spacer()
GeometryReader { proxy in
let frame = proxy.frame(in: .named("CustomSpace"))
Text("Hello, SwiftUI!")
.background(Color.blue)
.coordinateSpace(name: "CustomSpace")
.onAppear {
print("Actual Text Position: \(frame.debugDescription)")
}
}
.background(Color.red.opacity(0.2))
Spacer()
}
}
}

在这段代码中,Text定义了一个名为CustomSpace的自定义坐标空间。
这意味着 CustomSpace 的原点是 Text 的左上角。
输出的内容为:
Actual Text Position: (0.0, 67.0, 393.0, 743.0)
可以将这里的数值也进行换算为物理尺寸,那么得出的数值为:
(0.0, 201.0, 1179.0, 2229.0)

可以看出201的高度实际就是屏幕顶端到下面区域的距离,GeometryReader尺寸为1179 * 2229。因为0无法在绘图工具上描绘,因此这里的绿色是10像素宽。
这里存在的疑问就是输出的是(0.0,67.0)?
这个问题困扰了我将近两天的时间,我只能给出一个推测的结论。那就是coordinateSpace 只能在其子视图中计算位置,否则位置是从屏幕原点开始计算的。
这是 SwiftUI 的一个特性:
如果调用 proxy.frame(in:) 的视图本身不在命名坐标空间的作用范围内,那么它的计算会退回到全局坐标空间(屏幕坐标)。
CustomSpace 被绑定到 Text,但 GeometryReader 是 Text 的父视图,因此它的位置计算无法在 CustomSpace 坐标范围内完成。
总结
.coordinateSpace(name:) 在复杂布局中通过自定义坐标空间获取精确的位置和大小信息,非常适合处理需要跨视图交互的场景,如手势跟踪、动态布局计算等。
参考文章
Understanding frames and coordinates inside GeometryReader:https://www.hackingwithswift.com/books/ios-swiftui/understanding-frames-and-coordinates-inside-geometryreader
扩展知识
GeometryReader 测量的内容
在前面的知识分享中,通过添加GeometryReader测量CustomSpace的坐标空间:
struct ContentView: View {
var body: some View {
VStack {
Spacer()
GeometryReader { proxy in
let frame = proxy.frame(in: .global) // 或 .named("CustomSpace")
Text("Hello, SwiftUI!")
.background(Color.blue)
.coordinateSpace(name: "CustomSpace")
.onAppear {
print("Actual Text Position: \(frame.debugDescription)")
}
}
.background(Color.red.opacity(0.2))
Spacer()
}
}
}
删除Spacer()是如何影响布局的?
布局 1:有顶部和底部的 Spacer()
VStack {
Spacer()
GeometryReader { proxy in ... }
Spacer()
}
布局1规则:两个 Spacer() 会均匀分配剩余的可用垂直空间,GeometryReader 位于中间部分,且高度有限。
输出内容为:
Actual Text Position: (0.0, 67.0, 393.0, 743.0)
布局2:删除底部的Spacer()
VStack {
Spacer()
GeometryReader { proxy in ... }
}
布局规则:顶部 Spacer() 把可用空间挤到 GeometryReader 的下方,GeometryReader 占用剩余的空间。
输出的内容变成:
Actual Text Position: (0.0, 67.0, 393.0, 751.0)
高度 751.0 增加了 8.0,因为底部没有 Spacer() 挤占剩余空间。
布局 3:删除所有 Spacer()
VStack {
GeometryReader { proxy in ... }
}
布局规则:GeometryReader 占据了整个 VStack 的高度,因为没有其他视图竞争空间。
输出结果:
Actual Text Position: (0.0, 59.0, 393.0, 759.0)
高度 759.0 表示 GeometryReader 完全占用了整个父视图的可用空间。
Y 坐标变成 59.0,表明整个 VStack 开始的位置因为系统的默认边距(如 Safe Area)发生了偏移。
为什么 Y 坐标和高度发生变化?
(1) Y 坐标的变化
原因:GeometryReader 的 Y 坐标是它的顶部相对于全局屏幕(frame(in: .global))的垂直偏移。
Spacer() 的移除或添加会改变 GeometryReader 在 VStack 内的布局,从而影响它在全局坐标空间中的 Y 值。
(2) 高度的变化
Spacer() 控制父视图的剩余可用空间分配:
有 Spacer():GeometryReader 的高度被限制,因为 Spacer() 占用了部分空间。
无 Spacer():GeometryReader 会占据整个父视图的高度,因此高度增加。
Safe Area 的影响
接着上面的扩展问题进行叙述,当删除两个Spacer()后:
struct ContentView: View {
var body: some View {
VStack {
GeometryReader { proxy in
let frame = proxy.frame(in: .global) // 或 .named("CustomSpace")
Text("Hello, SwiftUI!")
.background(Color.blue)
.coordinateSpace(name: "CustomSpace")
.onTapGesture {
print("Tapped")
}
.onAppear {
print("Actual Text Position: \(frame.debugDescription)")
}
}
.background(Color.red.opacity(0.2))
}
}
}
可以发现Text的布局区域连接到了视图的顶部。

但是输出的frame中的Y坐标为59.0,而不是0.0。
Actual Text Position: (0.0, 59.0, 393.0, 759.0)
这是因为 GeometryReader 测量的全局坐标受到 Safe Area 的影响。以下是具体分析和解答:
为什么 Y 坐标是 59.0 而不是 0.0?
Safe Area 的影响:
Safe Area 是 iOS 用于防止视图被设备的物理部分(如状态栏、刘海、底部手势栏)遮挡的区域。
GeometryReader 所在的 VStack 会默认被限制在 Safe Area 内,因此 GeometryReader 的顶部起点在全局坐标中是 59.0,而不是屏幕的物理原点 0.0。
蓝色背景为什么看起来贴在顶部?
Text 的内容绘制(蓝色背景)并没有考虑 Safe Area,它仍然渲染在父视图的顶端。
这导致视觉上蓝色背景似乎连接到了顶部,但从 GeometryReader 测量的全局坐标来看,它是被 Safe Area 偏移了。
颠倒顺序的影响
VStack {
Text("Hello, SwiftUI!")
.coordinateSpace(name: "CustomSpace")
.background(
GeometryReader { proxy in
let frame = proxy.frame(in: .named("CustomSpace"))
Text("GeometryReader Frame in CustomSpace: \(frame)")
.onAppear {
print("GeometryReader Frame in CustomSpace: \(frame)")
}
}
)
在这段代码中,输出的内容为:
GeometryReader Frame in CustomSpace: (142.83333333333334, 428.3333333333333, 107.33333333333334, 20.333333333333314)
当改变coordinateSpace和background顺序时,
.background()
.coordinateSpace(name: "CustomSpace")
输出的内容就会变成:
GeometryReader Frame in CustomSpace: (-0.1666666666666572, 0.0, 107.33333333333333, 20.333333333333332)
问题原因:
1)背景的 GeometryReader 先被计算:
此时 CustomSpace 尚未被定义,因此 proxy.frame(in: .named(“CustomSpace”)) 会尝试查找一个尚未存在的坐标空间。
GeometryReader 的计算是基于其父视图(VStack)的坐标空间来进行的。
2)后定义 CustomSpace:
.coordinateSpace(name:) 在最后应用,意味着 CustomSpace 的原点和范围仅对 Text 及其后续子视图生效,但背景中的 GeometryReader 的布局已经确定。
为什么出现 -0.1666666666666572?
1、父视图的对齐影响:
默认情况下,Text 会在其父视图(VStack)中水平居中对齐。
Text 的宽度并非整除屏幕宽度(393px),因此可能出现浮点数舍入误差。
这里的 -0.1666666666666572 是由这种舍入误差引起的。
2、坐标空间的滞后:
GeometryReader 在 .coordinateSpace(name:) 之前渲染,意味着它并未感知 CustomSpace 的定义。
proxy.frame(in: .named(“CustomSpace”)) 会基于默认父视图坐标计算,导致其位置出现偏差。