在 SwiftUI 中实时获取移动视图的坐标

Get coordinates in real time of a moving view in SwiftUI

我正在移动一个名为 Point 的视图(基本上是一个点),该点每 2 秒在屏幕中移动一次,我需要实时更新坐标才能准确知道在哪个位置位置正在过渡。基本上,我告诉点从 (X, Y) 移动到 (newX, newY) 并在 0.2 秒内完成,但我正在尝试读取点的位置(我需要实时知道它,所以与此同时点在移动,我需要知道那个移动的位置,大约每秒 30 次是可以的(甚至 15 次),但坐标不会更新!

我尝试使用 GeometryReader 获取坐标并创建一个带有位置的 ObservableObject,然后执行坐标更新我尝试在 onChange() 方法中进行,但我无法使其工作(实际上,它根本不会在任何时候执行)。我还在我的模型中实现了 Equatable 协议,以便能够使用方法 onChange()

有人知道为什么 onChange() 没有被调用吗?实时显示移动点坐标的正确方案是什么?

我的SwiftUI代码(演示)是:

struct Point: View {
    var body: some View {
        ZStack{
            Circle()
                .frame(width: 40, height: 40, alignment: .center)
                .foregroundColor(.green)
            Circle()
                .frame(width: 5, height: 5, alignment: .center)
                .foregroundColor(.black)
        }
    }
}


struct Page: View {
    @ObservedObject var P: Position = Position()
    var body: some View {
        VStack {
            GeometryReader() { geo in
                Point()
                    .position(x: CGFloat(P.xObjective), y: CGFloat(P.yObjective))
                    .animation(.linear(duration: 0.2))
                    .onChange(of: P) { Equatable in
                        P.xRealtime = geo.frame(in: .global).midX
                        P.yRealTime = geo.frame(in: .global).midY
                        print("This should had been executed!")
                    }
            }
            Text("X: \(P.xRealtime), Y: \(P.yRealTime)")
        }.onAppear() {
            P.startMovement()
        }
    }
}

我的 Swift 代码(型号)是:

class Position: ObservableObject, Equatable {
    @Published var xObjective: CGFloat = 0.0
    @Published var yObjective: CGFloat = 0.0
    @Published var xRealtime: CGFloat = 0.0
    @Published var yRealTime: CGFloat = 0.0
    
    private var mainTimer: Timer = Timer()
    private var executedTimes: Int = 0
    
    private var coordinatesPoints: [(x: CGFloat, y: CGFloat)] {
        let screenWidth = UIScreen.main.bounds.width
        let screenHeight = UIScreen.main.bounds.height
        return [(screenWidth / 24 * 12 , screenHeight / 24 * 12),
                (screenWidth / 24 * 7 , screenHeight / 24 * 7),
                (screenWidth / 24 * 7 , screenHeight / 24 * 17)
        ]
    }
    
    // Conform to Equatable protocol
    static func == (lhs: Position, rhs: Position) -> Bool {
        if lhs.xRealtime == rhs.xRealtime && lhs.yRealTime == rhs.yRealTime && lhs.xObjective == rhs.xObjective && lhs.yObjective == rhs.yObjective {
            return true
        }
        return false
    }
    
    func startMovement() {
        mainTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(movePoint), userInfo: nil, repeats: true)
    }
    
    @objc func movePoint() {
        if (executedTimes == coordinatesPoints.count) {
            mainTimer.invalidate()
            return
        }
        self.xObjective = coordinatesPoints[executedTimes].x
        self.yObjective = coordinatesPoints[executedTimes].y
        executedTimes += 1
    }
}

您无法在 SwiftUI 中访问实时动画值。

相反,您可以通过计算每一帧的位置自己制作动画。 CADisplayLink 将帮助您:它是一个 Timer 模拟,但在每个帧渲染时调用,因此您可以更新您的值。

struct Page: View {
    @ObservedObject var P: Position = Position()
    var body: some View {
        VStack {
            Point()
                .position(x: P.realtimePosition.x, y: P.realtimePosition.y)
            Text("X: \(P.realtimePosition.x), Y: \(P.realtimePosition.y)")
        }.onAppear() {
            P.startMovement()
        }
    }
}

class Position: ObservableObject {
    struct AnimationInfo {
        let startDate: Date
        let duration: TimeInterval
        let startPoint: CGPoint
        let endPoint: CGPoint

        func point(at date: Date) -> (point: CGPoint, finished: Bool) {
            let progress = CGFloat(max(0, min(1, date.timeIntervalSince(startDate) / duration)))
            return (
                point: CGPoint(
                    x: startPoint.x + (endPoint.x - startPoint.x) * progress,
                    y: startPoint.y + (endPoint.y - startPoint.y) * progress
                ),
                finished: progress == 1
            )
        }
    }

    @Published var realtimePosition = CGPoint.zero

    private var mainTimer: Timer = Timer()
    private var executedTimes: Int = 0
    private lazy var displayLink: CADisplayLink = {
        let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
        displayLink.add(to: .main, forMode: .default)
        return displayLink
    }()
    private let animationDuration: TimeInterval = 0.1
    private var animationInfo: AnimationInfo?

    private var coordinatesPoints: [CGPoint] {
        let screenWidth = UIScreen.main.bounds.width
        let screenHeight = UIScreen.main.bounds.height
        return [CGPoint(x: screenWidth / 24 * 12, y: screenHeight / 24 * 12),
                CGPoint(x: screenWidth / 24 * 7, y: screenHeight / 24 * 7),
                CGPoint(x: screenWidth / 24 * 7, y: screenHeight / 24 * 17)
        ]
    }

    func startMovement() {
        mainTimer = Timer.scheduledTimer(timeInterval: 2.5,
            target: self,
            selector: #selector(movePoint),
            userInfo: nil,
            repeats: true)
    }

    @objc func movePoint() {
        if (executedTimes == coordinatesPoints.count) {
            mainTimer.invalidate()
            return
        }
        animationInfo = AnimationInfo(
            startDate: Date(),
            duration: animationDuration,
            startPoint: realtimePosition,
            endPoint: coordinatesPoints[executedTimes]
        )
        displayLink.isPaused = false
        executedTimes += 1
    }

    @objc func displayLinkAction() {
        guard
            let (point, finished) = animationInfo?.point(at: Date())
            else {
            displayLink.isPaused = true
            return
        }
        realtimePosition = point
        if finished {
            displayLink.isPaused = true
            animationInfo = nil
        }
    }
}