kike
kike

Reputation: 738

Get coordinates in real time of a moving view in SwiftUI

I am moving a view called Point (which is basically a point), the point is moving in the screen every 2 seconds, and I need to update the coordinates in real time to know exactly in which position is on the transition. Basically, I tell the point to move from (X, Y) to (newX, newY) and to make it in 0.2 seconds, but I am trying to read the position of the point (I need to know it in realtime, so meanwhile the point is moving, I need to know the position in that movement, something like a 30times per seconds is okay (even 15), but the coordinates does not get updated!

I tried getting the coordinates with the GeometryReader and create an ObservableObject with the position, then to execute the update of the coordinates I tried to do it in the onChange() method, but I can not make it work (indeed, it does not get executed any time at all). I also implement the Equatable protocol to my model to be able to use the method onChange()

Anyone knows why onChange() does not get called? which is the right solution to show the coordinates in real time of the moving point?

My SwiftUI code (presentation) is:

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()
        }
    }
}

My Swift code (model) is:

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
    }
}

Upvotes: 0

Views: 1216

Answers (1)

Phil Dukhov
Phil Dukhov

Reputation: 87904

You can't access realtime animation value in SwiftUI.

Instead you can animate it by yourself, by calculating position for each frame. CADisplayLink will help you with that: it's a Timer analogue, but is called on each frame render, so you can update your value.

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
        }
    }
}

Upvotes: 1

Related Questions