
大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。
图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG
我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。
展菲:您的前沿技术领航员
大家好,我是展菲!
全网搜索“展菲”,即可纵览我在各大平台的知识足迹。
公众号“Swift社区”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
微信端添加好友“fzhanfei”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!
文章目录
前言
如果你已经用过 SwiftUI 的 GeometryReader,大概率也写过这样的代码:
GeometryReader { geo in
Text("Hello")
.position(x: geo.size.width / 2, y: geo.size.height / 2)
}
然后你可能会想:
- 为什么我拿到的是
global和local两种坐标? named坐标空间到底什么时候用?- 我能不能获取一个子视图相对于另一个视图的位置?
- GeometryProxy 到底能做多强的事情?是不是只是读尺寸?
其实,GeometryProxy 比你想象的功能要强得多,它是 SwiftUI 中布局观察、位置感知、相对坐标计算的核心工具。
许多高级交互,比如:
- Sticky Header
- Section Header 进入视图动画
- 滚动侦测
- 折叠 Banner
- 粒子动画跟随滚动
- 光标跟随效果(Cursor Tracking)
……本质上都在用 GeometryProxy + 坐标空间。
这一篇我会带你从基础结构 → 坐标空间 → 实战技巧,一步一步把 GeometryProxy 玩通透。
GeometryProxy 到底是什么?
你可以把它理解成:
“当前视图的实时布局与位置快照。”
它能提供三类信息:
size:当前视图自身尺寸
safeAreaInsets:当前视图安全区信息
frame(in: CoordinateSpace):当前视图在不同坐标空间中的位置
.local.global.named("XXX")
其中第三个是最关键的:它让我们可以从任意参考系观察视图的位置。
frame(in:) 真实含义是什么?
官方的定义比较抽象,这里用一句话总结:
frame(in:) 给你这个视图的包围盒在某个坐标系下的坐标与尺寸。
也就是 BBox 信息。
比如:
geo.frame(in: .global).minY
表示当前视图距离屏幕顶部的偏移量。
这个属性是实现各种滚动监听与交互的根基。
三种坐标空间:local / global / named
这是 GeometryProxy 最常被忽略、但最重要的核心。
下面一口气解释清楚:
1).global:相对于整个屏幕
适合做滚动侦测、吸顶、折叠 Banner 等。
实际含义是:
在整个 UIWindow 坐标系中,这个视图的 frame 是什么?
你会用它来判断「视图滚出去了吗?」。
2).local:相对于 GeometryReader 自身
非常适合做内部布局逻辑,例如:
- 让一个元素始终在 Geometry 的中心
- 在容器内部做比例缩放动画
例如:
let center = geo.frame(in: .local).midX
这里 midX 永远是 GeometryReader 的一半。
3).named:自定义坐标空间(高级功能)
这个是最容易被忽略,但也是最强大的功能。
你可以把某个视图定义成“参考坐标系”,然后让子视图在这个参考系下计算位置。
核心写法:
ScrollView {
VStack {
...
}
.coordinateSpace(name: "scroll")
}
然后在内部:
geo.frame(in: .named("scroll"))
这可以实现:
- 子视图相对 ScrollView 内部的位移
- 区域性的滚动吸顶
- 判断某个 section 是否进入视口
- 追踪滚动的局部位置变化(更优雅)
这也是我们接下来所有 Demo 的基础。
可运行 Demo
把以下代码直接丢到 SwiftUI 工程里即可运行:
Demo:实时显示三种坐标空间的 minY 值
struct CoordinateSpaceDemo: View {
var body: some View {
ScrollView {
VStack(spacing: 30) {
GeometryReader { geo in
VStack(spacing: 12) {
Text("Global Y: \(Int(geo.frame(in: .global).minY))")
Text("Local Y: \(Int(geo.frame(in: .local).minY))")
Text("Named Y: \(Int(geo.frame(in: .named("scroll")).minY))")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.2))
.cornerRadius(12)
}
.frame(height: 120)
ForEach(0..<20) { i in
Text("Row \(i)")
.frame(maxWidth: .infinity)
.padding()
.background(Color.gray.opacity(0.1))
}
}
.padding()
}
.coordinateSpace(name: "scroll")
}
}
Demo 解释
.global会随着整个 ScrollView 滚动而不断变化.named("scroll")会随着当前 ScrollView 内部滚动而变化(不会受其他父级影响).local永远是 0,因为 GeometryReader 内部坐标不变
你会直观理解三者的差别。
高级技巧:判断某个 Section 是否正在进入屏幕
很多 App 都有这样的交互 —— 部分标题进入视口时触发动画:
- 进入视口 → 拉大 → 高亮
- 滑出视口 → 变淡
- 停留在视口 → 吸附在顶部
我们来写一个可复用的 Section 可见性检测器。
Demo:判断 Section 是否进入可视区
struct VisibilityDemo: View {
@State private var visible = false
var body: some View {
ScrollView {
VStack(spacing: 60) {
Color.clear.frame(height: 50) // 空间
VisibilityDetector { isVisible in
visible = isVisible
}
.frame(height: 60)
.background(visible ? Color.green : Color.red)
.cornerRadius(10)
.padding()
ForEach(0..<15) { i in
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.2))
.frame(height: 100)
.padding(.horizontal)
}
}
.padding(.top)
}
.coordinateSpace(name: "scroll")
}
}
struct VisibilityDetector: View {
var onChange: (Bool) -> Void
var body: some View {
GeometryReader { geo in
Color.clear
.onChange(of: geo.frame(in: .named("scroll")).minY) { value in
let isVisible = value > 0 && value < UIScreen.main.bounds.height
onChange(isVisible)
}
}
}
}
场景应用
- 监听广告 Banner 进入视口 → 自动播放视频
- Section 进入视口 → 导航栏高亮
- 内容页:滚动到评论区时触发评论展示动画
子视图相对于另一个视图的位置怎么计算?
这是许多人不知道 GeometryProxy 还能做到的事情。
原理:
frame(in: .named(A)) - frame(in: .named(B))
你可以把两个视图分别装进两个坐标空间,然后做差值计算。
Demo:让一个小球始终跟随另一个视图的位置
struct RelativePositionDemo: View {
@State private var targetY: CGFloat = 0
var body: some View {
VStack(spacing: 40) {
GeometryReader { geo in
Color.clear
.preference(key: ScrollOffsetKey.self,
value: geo.frame(in: .named("area")).midY)
}
.frame(height: 60)
.background(Color.orange.opacity(0.3))
.onPreferenceChange(ScrollOffsetKey.self) { targetY = $0 }
Circle()
.fill(Color.blue)
.frame(width: 40, height: 40)
.offset(y: targetY - 200)
}
.frame(height: 400)
.background(Color.gray.opacity(0.1))
.coordinateSpace(name: "area")
}
}
效果非常直观:
- 上面那条橙色 Bar 的中心点在哪里
- 下面那颗小球就会跟着移动到同样的相对位置
这是许多交互动画的基础,比如:
- 雷达扫描
- 射线跟随效果
- 镜像联动动画
实战场景:GeometryProxy 不是“读尺寸”,而是“读位置 + 读关系”
下面列几个真实项目中常常遇到的场景。
场景 1:视频 App(仿抖音 / 油管)
- 滚动到某个 cell 时,判断是否进入视口 → 自动播放
- 视频滑走后自动暂停
- 顶部缩放 Header
全都需要 GeometryProxy 的 global 坐标。
场景 2:电商详情页
- Banner 折叠收缩
- 规格 Tab 吸顶
- 当前 Section 高亮
用的是 .named("scroll")。
场景 3:复杂交互动画
- 视图相对位置变化 → 动画驱动
- 元素跟随效果
- 三维滚动视差(Parallax)
用的是 frame(in:) 做数学计算。
总结
如果用一句话总结 GeometryProxy:
GeometryProxy 是 SwiftUI 世界里的“位置传感器 + 布局尺子”。
掌握它,你可以:
- 监听滚动位置
- 做吸顶导航
- 做折叠 Header
- 做透明渐变导航
- 检测元素可见性
- 计算视图之间的相对位置
- 做高阶滚动驱动动画(Parallax、跟随、折叠、吸附)
而坐标空间(CoordinateSpace)是整个工作机制的灵魂:
| 坐标空间 | 用途 |
|---|---|
.global | 相对屏幕 → 滚动动画、吸顶 |
.local | 相对容器 → 内部布局、动画中心点 |
.named | 自定义参考系 → Section 可见性、局部滚动 |
浙公网安备 33010602011771号