SwiftUI GeometryReader在onAppear触发时与后续更新不一致的问题
SwiftUI GeometryReader在onAppear触发时与后续更新不一致的问题

SwiftUI GeometryReader在onAppear触发时与后续更新不一致的问题

问题描述

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

刚开始的方块位置还可以跟随手势放置,再次生成的方块位置就会存在一个大概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 值。

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

发表回复

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