0

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
    }
}
kike
  • 658
  • 1
  • 8
  • 26
  • I am lost with this. Don't you already have the location of the point? Also, if you need precision with your timer, remember `Timer()` is not guaranteed to fire on time or equally. – Yrb Sep 11 '21 at 20:53
  • I need also the position when is transitioning, and I need it in real time. If timer does not give me the exact and precise execution, what does it? I need it to be as precise as possible – kike Sep 11 '21 at 21:03
  • `CADisplayLink()`. It calls your method every time the screen is redrawn. It is the most precise thing in this situation. If the system gets busy, `Timer()' gets put off, so the firing gets erratic. Now, are you saying you are trying to get the position that shows when the `.animation()` is running? – Yrb Sep 11 '21 at 21:17
  • Timer the only thing that does is to change the position of the point every X seconds, so what I need is to get the position in the transition between, sounds like when the interface is redrawn is the way to go, let me check that works – kike Sep 11 '21 at 21:44
  • What would be use case? If your codes works it would show a point moving down-right, what is the use of it? – ios coder Sep 11 '21 at 22:42
  • I just have the need to know, when I animate a view (any view, the point is just an example), I need to get the coordinates in real time meanwhile is moving. – kike Sep 12 '21 at 00:18
  • You need to read up on animations. The way they work is the view itself has already moved. The animation just makes it seem like it is moving on some path. I am not sure you will ever get the location of the animation. If you want that sort of thing, I would use SpriteKit. – Yrb Sep 12 '21 at 03:24

1 Answers1

1

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
        }
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220