问题描述
在制作方块游戏时,发现当放置完现有的方块后,再次生成的方块就会存在一个偏差。

刚开始的方块位置还可以跟随手势放置,再次生成的方块位置就会存在一个大概150的上下偏差。
我在使用公式套用多次,也没有计算到150是从哪里来的。
问题定位

后来在多次测试中发现,第一批生成的方块输出的距离视图左上角原点的位置为:
// 第一次生成的方块距离视图左上角原地的距离
blockOrigins[0]:(7.5, 456.0)
// 第二次生成的方块距离视图左上角原地的距离
blockOrigins[0]:(24.0, 605.6666666666666)
可以看到,第二次生成的方块的GeometryReader值,发生了变化。
DraggableBlockView(block: block) { start, end in
placeBlock(block, start, end, item)
}
.overlay {
GeometryReader { geo in
Color.clear
.onAppear {
let geoPosition = geo.frame(in: .global).origin
blockOrigins[item] = geoPosition
print("方块 \(item) 的位置为:\(geoPosition)")
print("blockOrigins[item]:\(blockOrigins[item])")
}
}
}
因此,为了测试GeometryReader值什么时候发生的变化,我尝试使用onChange(of:) 监听 geoPosition 变化。
onChange(of:) 监听 geoPosition 变化
DraggableBlockView(block: block) { start, end in
placeBlock(block, start, end, item)
}
.overlay {
GeometryReader { geo in
Color.clear
.onAppear {
let geoPosition = geo.frame(in: .global).origin
blockOrigins[item] = geoPosition
print("方块 \(item) 的位置为:\(geoPosition)")
print("blockOrigins[item]:\(blockOrigins[item])")
}
.onChange(of: geo.frame(in: .global).origin) { newValue in
print("newValue:\(newValue)")
}
}
}
当我尝试使用onChange检测时,我发现Xcode并没有实际的输出,也就意味着没有检测到GeometryReader值的变化。
定期轮询 geoPosition
因为onChange(of:) 无法捕捉所有变化,所以使用 Timer 定期检查 geoPosition:
@State private var timer: Timer?
DraggableBlockView(block: block) { start, end in
placeBlock(block, start, end, item)
}
.overlay {
GeometryReader { geo in
Color.clear
.onAppear {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
let geoPosition = geo.frame(in: .global).origin
blockOrigins[item] = geoPosition
print("方块 \(item) 的位置为:\(geoPosition)")
print("blockOrigins[item]:\(blockOrigins[item])")
}
}
}
}
当使用timer时,发现Xcode只会输出前面第二次距离视图原点的坐标 605,而不是 456。
// 使用timer输出
blockOrigins[0]:(24.0, 605.6666666666666)
但是,当我把geoPosition放到timer外面时:
let geoPosition = geo.frame(in: .global).origin
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
blockOrigins[item] = geoPosition
print("方块 \(item) 的位置为:\(geoPosition)")
print("blockOrigins[item]:\(blockOrigins[item])")
}
Xcode输出又变成了
blockOrigins[0]:(7.5, 456.0)
问题原因
经过上述问题的排查,发现问题的本质是geo.frame(in: .global).origin 的值在 onAppear 触发时与后续更新时不同。这通常与 SwiftUI 布局顺序、异步更新、以及 GeometryReader 读取的时机 有关。
为什么 geo.frame(in: .global).origin 发生变化?
1、onAppear 触发的时机
onAppear 在 视图首次渲染完成后执行,此时布局可能还未完全稳定,导致 geo.frame(in: .global).origin 可能并不是最终值。
如果 ZStack 内部的内容稍后发生重新布局(比如刷新视图、动画、或 State 变化),geo.frame(in: .global).origin 可能会改变。
2、Timer 读取的值不同
let geoPosition = geo.frame(in: .global).origin 放在 timer 外面时:
geoPosition 只在 onAppear 触发时计算一次。
由于布局未稳定,这个初始值可能与后续 SwiftUI 计算的最终值不同。
let geoPosition = geo.frame(in: .global).origin 放在 timer 里面时:
geoPosition 每次 Timer 触发都会重新计算。
这意味着 blockOrigins[item] 始终是最终计算出的稳定值。
解决方案
为了确保 geo.frame(in: .global).origin 是最终稳定的值,使用 DispatchQueue.main.async 延迟获取。
.onAppear {
DispatchQueue.main.async {
let geoPosition = geo.frame(in: .global).origin
blockOrigins[item] = geoPosition
print("延迟 1 帧后获取的位置: \(geoPosition)")
}
}
在使用DispatchQueue.main.async后,输出的只有605这个稳定的值。
blockOrigins[0]:(24.0, 605.6666666666666)
总结
在 SwiftUI 布局中,使用 GeometryReader 读取视图的 frame(in: .global).origin 位置时,会发现初始获取的位置与后续刷新视图后的位置不同。
这是因为当onAppear 触发时获取的 geo.frame(in: .global).origin 可能不是最终位置,可能是一个不稳定的值。
这涉及到SwiftUI 的异步布局机制:
1)onAppear 触发的时机是在视图刚刚加载后,但 SwiftUI 可能还在调整布局,此时 geo.frame(in: .global).origin 可能并未最终确定。
2)视图重新渲染时(例如数据变化或交互),布局可能再次调整,导致 geo.frame(in: .global).origin 发生变化。
解决方案为:使用 DispatchQueue.main.async 延迟获取位置。让 SwiftUI 先完成一帧布局,再获取 GeometryReader 提供的 frame 信息,避免 onAppear 过早读取错误的 origin 值。