SwiftUI自定义坐标空间修饰符coordinateSpace
SwiftUI自定义坐标空间修饰符coordinateSpace

SwiftUI自定义坐标空间修饰符coordinateSpace

.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”)) 会基于默认父视图坐标计算,导致其位置出现偏差。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注