diff --git a/apps/ios/CrossDeskMobile/Input/RemoteTouchInputView.swift b/apps/ios/CrossDeskMobile/Input/RemoteTouchInputView.swift index 23c4b73c..fd5460b7 100644 --- a/apps/ios/CrossDeskMobile/Input/RemoteTouchInputView.swift +++ b/apps/ios/CrossDeskMobile/Input/RemoteTouchInputView.swift @@ -58,6 +58,7 @@ struct RemoteTouchInputView: UIViewRepresentable { let videoSize: CGSize let controlMode: MouseControlMode let remoteCursorPosition: CGPoint? + let remoteCursorPositionRevision: UInt64 let viewportScale: CGFloat let viewportOffset: CGSize let viewportChanged: (CGFloat, CGSize) -> Void @@ -81,7 +82,8 @@ struct RemoteTouchInputView: UIViewRepresentable { view.onLeftUp = leftUp view.onRightClick = rightClick view.onScroll = scroll - view.synchronizeRemoteCursor(remoteCursorPosition) + view.synchronizeRemoteCursor(remoteCursorPosition, + revision: remoteCursorPositionRevision) } } @@ -108,6 +110,7 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate { private var relativeCursorPoint: (Float, Float) = (0.5, 0.5) private var lastRemoteCursorPoint: (Float, Float)? + private var lastRemoteCursorRevision: UInt64? private var relativePanActive = false private var heldDragPoint: (Float, Float)? private var heldDragLastLocation: CGPoint? @@ -184,7 +187,12 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate { viewportOffset = constrainedViewportOffset(offset, scale: nextScale) } - func synchronizeRemoteCursor(_ position: CGPoint?) { + func synchronizeRemoteCursor(_ position: CGPoint?, revision: UInt64) { + // SwiftUI refreshes this view for video frames and shape-only feedback + // too. Consume each position update once, even when it arrives during + // local movement, so a later refresh cannot restore an old coordinate. + guard revision != lastRemoteCursorRevision else { return } + lastRemoteCursorRevision = revision guard let position else { lastRemoteCursorPoint = nil return @@ -194,7 +202,11 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate { Float(min(max(position.y, 0), 1)) ) lastRemoteCursorPoint = point - applyRemoteCursorIfIdle() + guard controlMode == .relative, + !relativePanActive, + heldDragPoint == nil, + hoverLastLocation == nil else { return } + relativeCursorPoint = point } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, @@ -400,7 +412,6 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate { } default: relativePanActive = false - applyRemoteCursorIfIdle() } } @@ -429,7 +440,6 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate { } heldDragPoint = nil heldDragLastLocation = nil - applyRemoteCursorIfIdle() default: break } @@ -457,14 +467,6 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate { return relativeCursorPoint } - private func applyRemoteCursorIfIdle() { - guard controlMode == .relative, - !relativePanActive, - heldDragPoint == nil, - let point = lastRemoteCursorPoint else { return } - relativeCursorPoint = point - } - private func normalized(_ point: CGPoint) -> (Float, Float)? { guard let renderRect = renderRect(), renderRect.contains(point) else { return nil } return (Float((point.x - renderRect.minX) / renderRect.width), diff --git a/apps/ios/CrossDeskMobile/RemoteSessionModel.swift b/apps/ios/CrossDeskMobile/RemoteSessionModel.swift index ba2542ae..d3af8546 100644 --- a/apps/ios/CrossDeskMobile/RemoteSessionModel.swift +++ b/apps/ios/CrossDeskMobile/RemoteSessionModel.swift @@ -381,6 +381,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe @Published private(set) var remoteCursorVisible = true @Published private(set) var remoteCursorShape = 0 @Published private(set) var remoteCursorPosition: CGPoint? + @Published private(set) var remoteCursorPositionRevision: UInt64 = 0 @Published private(set) var remoteCursorVisualOffset = CGPoint.zero @Published private(set) var hasRemoteCursorState = false @Published private(set) var bitrate: UInt = 0 @@ -775,6 +776,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe remoteCursorVisible = true remoteCursorShape = 0 remoteCursorPosition = nil + remoteCursorPositionRevision &+= 1 remoteCursorVisualOffset = .zero hasRemoteCursorState = false remoteCursorSequence = nil @@ -991,6 +993,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe } else { remoteCursorPosition = nil } + remoteCursorPositionRevision &+= 1 } hasRemoteCursorState = true } diff --git a/apps/ios/CrossDeskMobile/RemoteSessionView.swift b/apps/ios/CrossDeskMobile/RemoteSessionView.swift index 6213e538..ecd271c0 100644 --- a/apps/ios/CrossDeskMobile/RemoteSessionView.swift +++ b/apps/ios/CrossDeskMobile/RemoteSessionView.swift @@ -79,6 +79,7 @@ struct RemoteSessionView: View { videoSize: session.displayGeometrySize, controlMode: session.mouseControlMode, remoteCursorPosition: session.remoteCursorPosition, + remoteCursorPositionRevision: session.remoteCursorPositionRevision, viewportScale: viewport.scale, viewportOffset: viewport.offset, viewportChanged: { scale, offset in @@ -161,8 +162,8 @@ struct RemoteSessionView: View { .onChange(of: session.mouseControlMode) { _ in cursorPosition = session.remoteCursorPosition } - .onChange(of: session.remoteCursorPosition) { position in - guard let position else { return } + .onChange(of: session.remoteCursorPositionRevision) { _ in + guard let position = session.remoteCursorPosition else { return } cursorPosition = position } .fileImporter(isPresented: $showingFileImporter,