Reputation: 738
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
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