网罗开发(小红书、快手、视频号同名)

  大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括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)
}

然后你可能会想:

  • 为什么我拿到的是 globallocal 两种坐标?
  • named 坐标空间到底什么时候用?
  • 我能不能获取一个子视图相对于另一个视图的位置?
  • GeometryProxy 到底能做多强的事情?是不是只是读尺寸?

其实,GeometryProxy 比你想象的功能要强得多,它是 SwiftUI 中布局观察、位置感知、相对坐标计算的核心工具。
许多高级交互,比如:

  • Sticky Header
  • Section Header 进入视图动画
  • 滚动侦测
  • 折叠 Banner
  • 粒子动画跟随滚动
  • 光标跟随效果(Cursor Tracking)

……本质上都在用 GeometryProxy + 坐标空间。

这一篇我会带你从基础结构 → 坐标空间 → 实战技巧,一步一步把 GeometryProxy 玩通透。

GeometryProxy 到底是什么?

你可以把它理解成:

“当前视图的实时布局与位置快照。”

它能提供三类信息:

  1. size:当前视图自身尺寸

  2. safeAreaInsets:当前视图安全区信息

  3. 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 可见性、局部滚动