-
Notifications
You must be signed in to change notification settings - Fork 153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Can't animate moving MapViewAnnotation
#2213
Comments
Hi, thank you for the report. Can you please share your use-case - do you animate just from A to B linearly, or do animate along the path? The support of I've created an internal ticket for further investigation https://mapbox.atlassian.net/browse/MAPSIOS-1510 As a workaround, you can implement them similarly to the UIKit example: import SwiftUI
import MapboxMaps
@Observable
class AnimatedCoordinate {
private(set) var value: CLLocationCoordinate2D
private var animator: Animator?
init(initial value: CLLocationCoordinate2D) {
self.value = value
}
func animate(to coordinate: CLLocationCoordinate2D, duration: TimeInterval) {
animator = Animator(duration: duration, start: value, end: coordinate, handler: { [weak self] in
self?.value = $0
}, onEnd: { [weak self] in
self?.animator = nil
})
}
}
private class Animator {
private let startTime = CACurrentMediaTime()
private let link = Link()
private let duration: TimeInterval
private let handler: (CLLocationCoordinate2D) -> Void
private let endHandler: () -> Void
private let start: CLLocationCoordinate2D
private let end: CLLocationCoordinate2D
private let distance: LocationDistance
private class Link: NSObject {
private var impl: CADisplayLink?
var callback: (() -> Void)?
override init() {
super.init()
impl = CADisplayLink(target: self, selector: #selector(animateNextStep))
impl!.add(to: .main, forMode: .default)
}
@objc func animateNextStep() {
callback?()
}
func invalidate() {
impl?.invalidate()
impl = nil
}
deinit {
invalidate()
}
}
init(duration: TimeInterval, start: CLLocationCoordinate2D, end: CLLocationCoordinate2D, handler: @escaping (CLLocationCoordinate2D) -> Void, onEnd: @escaping () -> Void) {
self.start = start
self.end = end
self.distance = start.distance(to: end)
self.handler = handler
self.endHandler = onEnd
self.duration = duration
link.callback = { [weak self] in
self?.animateNextStep()
}
}
private func animateNextStep() {
let progress = (CACurrentMediaTime() - startTime) / duration
let currentDistanceOffset = distance * min(progress, 1)
if let coord = LineString([start, end]).coordinateFromStart(distance: currentDistanceOffset) {
handler(coord)
}
if progress >= 1 {
link.invalidate()
endHandler()
}
}
} Usage: struct MyMap: View {
@State private var coordinate = AnimatedCoordinate(initial: .helsinki)
var body: some View {
Map {
MapViewAnnotation(coordinate: coordinate.value) {
Text("🚀")
}
}
.onMapTapGesture { ctx in
coordinate.animate(to: ctx.coordinate, duration: 2)
}
}
} |
@persidskiy thanks for the work around. This is helpful. |
@persidskiy coming back to this after some testing. I'm seeing the For more context, I'm want to animate moving Here's my implementation, it's not the same as what you have but mechanistically the same: @_spi(Experimental) import MapboxMaps
import SwiftUI
import UIKit
import Combine
@MainActor
extension CADisplayLink {
static func durations() -> AsyncStream<CFTimeInterval> {
AsyncStream { continuation in
let displayLink = DisplayLink { displayLink in
continuation.yield(displayLink.targetTimestamp - displayLink.timestamp)
}
continuation.onTermination = { _ in
Task { await displayLink.stop() }
}
}
}
@MainActor
private class DisplayLink: NSObject {
private var displayLink: CADisplayLink!
private let handler: (CADisplayLink) -> Void
init(mode: RunLoop.Mode = .default, handler: @escaping (CADisplayLink) -> Void) {
self.handler = handler
super.init()
displayLink = CADisplayLink(target: self, selector: #selector(handle(displayLink:)))
displayLink.add(to: .main, forMode: mode)
}
func stop() {
displayLink.invalidate()
}
@objc func handle(displayLink: CADisplayLink) {
handler(displayLink)
}
}
}
struct MapView: View {
@State private var viewport = Viewport.followPuck(zoom: 18, bearing: .heading).padding(.top, 400)
@State private var userLocationViewModel = UserLocationViewModel()
var body: some View {
MapReader { proxy in
Map(viewport: $viewport) {
if let userLocation = userLocationViewModel.curAnimatedLoc {
MapViewAnnotation(coordinate: userLocation) {
// Custom view
}
.allowOverlap(true)
.allowOverlapWithPuck(true)
.ignoreCameraPadding(true)
}
}
.presentsWithTransaction(true)
.onStyleLoaded { [unowned userLocationViewModel] _ in
guard let locationManager = proxy.location else { fatalError("locationManager not found") }
locationManager.onLocationChange.observe { [unowned userLocationViewModel] locations in
userLocationViewModel.setTargetLocation(locations.last!.coordinate)
}.store(in: &userLocationViewModel.cancellables)
}
.ignoresSafeArea()
.task {
for await duration in CADisplayLink.durations() {
userLocationViewModel.animateOneFrame(duration: duration)
}
}
}
}
@Observable
class UserLocationViewModel {
var curAnimatedLoc: CLLocationCoordinate2D?
@ObservationIgnored private var latitudeRateOfChange: Double = 0.0
@ObservationIgnored private var longitudeRateOfChange: Double = 0.0
@ObservationIgnored private var progress: CFTimeInterval = 0.0
@ObservationIgnored var cancellables = Set<AnyCancellable>()
func setTargetLocation(_ loc: CLLocationCoordinate2D) {
guard let cur = curAnimatedLoc else {
curAnimatedLoc = loc
return
}
// Assuming that the average location update frequency is once per sec
latitudeRateOfChange = loc.latitude - cur.latitude
longitudeRateOfChange = loc.longitude - cur.longitude
progress = 0
}
func animateOneFrame(duration: CFTimeInterval) {
guard progress < 1, curAnimatedLoc != nil else {
return
}
curAnimatedLoc?.latitude += latitudeRateOfChange * duration
curAnimatedLoc?.longitude += longitudeRateOfChange * duration
progress += duration
}
}
} Do you foresee any fix to it? |
Hey @persidskiy, any updates here? |
Observed behavior and steps to reproduce
Changing the
coordinate
of aMapViewAnnotation
viawithAnimation
causes the view to re-appear instead of moving to the new location.Expected behavior
The annotation should move to the new location.
Notes / preliminary analysis
Seems to have been possible in UIKit examples but the behavior seems different in SwiftUI.
The text was updated successfully, but these errors were encountered: