Suivre la rotation de MKMapView

J'ai un MapKitView qui a une annotation qui pointe dans une certaine direction. Mon problème est que lorsque l'user fait pivoter la carte avec deux doigts, ou si la carte pivote pour suivre le titre de l'user, mon symbole doit être pivoté (ce qui n'est pas le cas car ils sont alignés à l'écran).

Je sais que je peux faire pivoter les symboles par l'opposé du titre de la camera cartographique. Je sais que je peux être informé des changements dans le titre de l'user pour faire pivoter l'annotation dans ce cas.

Mon problème est que je ne peux pas find un moyen de suivre la rotation de la carte en raison de la rotation interactive de l'user.

Je peux suivre le début et la fin des changements de région de carte, mais pas les changements entre les deux. J'ai essayé d'utiliser le KVO avec le roulement de la camera mais je ne reçois rien. J'ai essayé de chercher des notifications envoyées par le système, mais encore une fois, rien.

Quelqu'un at-il des suggestions sur la façon de suivre de manière fiable la rotation de la carte actuelle?

Malheureusement MapKit lui-même ne fournit aucune solution pour suivre les changements de rotation. Il fournit des events seulement au début et à la fin de la rotation. Et même plus: il ne met pas à jour la valeur du cap pour la camera, tout en faisant tourner la carte.

J'ai eu la même nécessité et créé une solution propre à Swift, qui a fonctionné pour moi.

1. Sous-class MKMapView pour traiter ses données

La partie la plus simple:

class MyMap : MKMapView { } 

2. Trouver l'object, qui a de manière fiable la valeur réelle de rotation de la carte

MKMapView est un type de conteneur UIView, qui contient un type de canevas à l'intérieur, où la carte est rendue puis transformée. J'ai recherché MKMapView pendant l'exécution, en explorant ses sous-vues. Le canevas a le nom de class MKScrollContainerView . Vous devez contrôler l'instance, donc vous:

  1. append l'object à la class
  2. écrire la fonction pour find cet object dans MKMapView
  3. find la canvas et la sauvegarder

Le code:

 class MyMap : MKMapView { var mapContainerView : UIView? init() { ... self.mapContainerView = self.findViewOfType("MKScrollContainerView", inView: self) ... } func findViewOfType(type: Ssortingng, inView view: UIView) -> UIView? { // function scans subviews recursively and returns reference to the found one of a type if view.subviews.count > 0 { for v in view.subviews { if v.dynamicType.description() == type { return v } if let inSubviews = self.findViewOfType(type, inView: v) { return inSubviews } } return nil } else { return nil } } } 

3. Calculer la rotation

MKScrollContainerView est pivoté simplement en changeant sa propriété de transform . La masortingce de rotation, utilisée à cette fin, est décrite dans la documentation d'Apple: https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/CGAffineTransform/#//apple_ref/c/func/CGAffineTransformMakeRotation

Cela ressemble à ceci:

  cosA sinA 0 -sinA cosA 0 0 0 1 

La fonction, pour calculer la rotation, basée sur cette masortingce, ressemble à ceci:

 class MyMap : MKMapView { ... func getRotation() -> Double? { // function gets current map rotation based on the transform values of MKScrollContainerView if self.mapContainerView != nil { var rotation = fabs(180 * asin(Double(self.mapContainerView!.transform.b)) / M_PI) if self.mapContainerView!.transform.b <= 0 { if self.mapContainerView!.transform.a >= 0 { // do nothing } else { rotation = 180 - rotation } } else { if self.mapContainerView!.transform.a <= 0 { rotation = rotation + 180 } else { rotation = 360 - rotation } } return rotation } else { return nil } } ... } 

4. Suivre la rotation constamment

La seule façon que j'ai trouvé pour faire ceci est d'avoir une boucle infinie, qui vérifie la valeur de rotation à chaque appel de boucle. Pour mettre en œuvre ce dont vous avez besoin:

  1. MyMap écouteur
  2. fonction, pour vérifier la rotation
  3. timer, pour appeler la fonction toutes les X secondes

Voici ma mise en œuvre:

 @objc protocol MyMapListener { optional func onRotationChanged(rotation rotation: Double) // message is sent when map rotation is changed } class MyMap : MKMapView { ... var changesTimer : NSTimer? // timer to track map changes; nil when changes are not tracked var listner : MyMapListener? var rotation : Double = 0 // value to track rotation changes ... func trackChanges() { // function detects map changes and processes it if let rotation = self.getRotation() { if rotation != self.rotation { self.rotation = rotation self.listner?.onRotationChanged(rotation: rotation) } } } func startTrackingChanges() { // function starts tracking map changes if self.changesTimer == nil { self.changesTimer = NSTimer(timeInterval: 0.1, target: self, selector: #selector(MyMap.trackChanges), userInfo: nil, repeats: true) NSRunLoop.currentRunLoop().addTimer(self.changesTimer!, forMode: NSRunLoopCommonModes) } } func stopTrackingChanges() { // function stops tracking map changes if self.changesTimer != nil { self.changesTimer!.invalidate() self.changesTimer = nil } } } 

C'est tout 😉

Vous pouvez download un exemple de projet dans mon repo: https://github.com/d-babych/mapkit-wrap

Vous pourriez essayer de créer un CADisplayLink , qui lancera un sélecteur sur les rafraîchissements de l'écran, ce qui sera suffisant pour synchroniser chaque image des animations de MapKit. À chaque passage, vérifiez la valeur de direction et mettez à jour votre vue d'annotation.

Il semble qu'il n'existe aucun moyen de suivre le simplement lire l'en-tête en cours de rotation de la carte. Depuis que je viens de mettre en place une vue de la boussole qui tourne avec la carte, je veux partager mes connaissances avec vous.

Je vous invite explicitement à affiner cette réponse. Comme j'ai une date limite, je suis satisfait comme c'est le cas maintenant (avant cela, la boussole n'était réglée que lorsque la carte s'est arrêtée pour tourner) mais il y a place à l'amélioration et à la mise au point.

J'ai téléchargé un exemple de projet ici: Projet exemple MapRotation

D'accord, commençons. Puisque je suppose que vous utilisez tous les Storyboards de nos jours, faites glisser quelques reconnaisseurs de gestes sur la carte. (Ceux qui ne savent sûrement pas comment convertir ces étapes en lignes écrites.)

Pour détecter la rotation de la carte, le zoom et l'angle 3D, nous avons besoin d'une rotation, d'un panoramique et d'un outil de reconnaissance gestuelle. Faites glisser les identifiants de gestes sur MapView

Désactiver "Retards touches terminés" pour la reconnaissance de mouvement de rotation … Désactiver "Retards touches terminés" pour le Détecteur de mouvement de rotation

… et augmentez les "Touches" à 2 pour la reconnaissance de gestes de pan. Augmentez le nombre de "Touches" à 2 pour la reconnaissance de mouvements de mouvements

Définissez le délégué de ces 3 sur le controller de vue conteneur. Ctrl-glisser vers le contrôleur de vue contenant ... ... et définissez le délégué.

Faites glisser pour les 3 reconnaisseurs de gestes les collections de references de sortie vers MapView et select "gestureRecognizers"

entrez la description de l'image ici

Maintenant faites Ctrl-glisser le reconnaisseur de mouvement de rotation à l'implémentation comme Outlet comme ceci:

 @IBOutlet var rotationGestureRecognizer: UIRotationGestureRecognizer! 

et tous les 3 reconnaisseurs comme IBAction:

 @IBAction func handleRotation(sender: UIRotationGestureRecognizer) { ... } @IBAction func handleSwipe(sender: UIPanGestureRecognizer) { ... } @IBAction func pinchGestureRecognizer(sender: UIPinchGestureRecognizer) { ... } 

Oui, j'ai nommé le geste de panoramique "handleSwype". C'est expliqué ci-dessous. 🙂

Énuméré ci-dessous le code complet pour le controller qui, bien sûr, doit également implémenter le protocole MKMapViewDelegate. J'ai essayé d'être très détaillé dans les commentaires.

 // compassView is the container View, // arrowImageView is the arrow which will be rotated @IBOutlet weak var compassView: UIView! var arrowImageView = UIImageView(image: UIImage(named: "Compass")!) override func viewDidLoad() { super.viewDidLoad() compassView.addSubview(arrowImageView) } // ****************************************************************************************** // * // Helper: Detect when the MapView changes * private func mapViewRegionDidChangeFromUserInteraction() -> Bool { let view = mapView!.subviews[0] // Look through gesture recognizers to determine whether this region // change is from user interaction if let gestureRecognizers = view.gestureRecognizers { for recognizer in gestureRecognizers { if( recognizer.state == UIGestureRecognizerState.Began || recognizer.state == UIGestureRecognizerState.Ended ) { return true } } } return false } // * // ****************************************************************************************** // ****************************************************************************************** // * // Helper: Needed to be allowed to recognize gestures simultaneously to the MapView ones. * func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool { return true } // * // ****************************************************************************************** // ****************************************************************************************** // * // Helper: Use CADisplayLink to fire a selector at screen refreshes to sync with each * // frame of MapKit's animation private var displayLink : CADisplayLink! func setUpDisplayLink() { displayLink = CADisplayLink(target: self, selector: "refreshCompassHeading:") displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes) } // * // ****************************************************************************************** // ****************************************************************************************** // * // Detect if the user starts to interact with the map... * private var mapChangedFromUserInteraction = false func mapView(mapView: MKMapView, regionWillChangeAnimated animated: Bool) { mapChangedFromUserInteraction = mapViewRegionDidChangeFromUserInteraction() if (mapChangedFromUserInteraction) { // Map interaction. Set up a CADisplayLink. setUpDisplayLink() } } // * // ****************************************************************************************** // * // ... and when he stops. * func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) { if mapChangedFromUserInteraction { // Final transform. // If all calculations would be correct, then this shouldn't be needed do nothing. // However, if something went wrong, with this final transformation the compass // always points to the right direction after the interaction is finished. // Making it a 500 ms animation provides elasticity und prevents hard transitions. UIView.animateWithDuration(0.5, animations: { self.arrowImageView.transform = CGAffineTransformMakeRotation(CGFloat(M_PI * -mapView.camera.heading) / 180.0) }) // You may want this here to work on a better rotate out equation. :) let stoptime = NSDate.timeIntervalSinceReferenceDate() print("Needed time to rotate out:", stoptime - startRotateOut, "with velocity", remainingVelocityAfterUserInteractionEnded, ".") print("Velocity decrease per sec:", (Double(remainingVelocityAfterUserInteractionEnded) / (stoptime - startRotateOut))) // Clean up for the next rotation. remainingVelocityAfterUserInteractionEnded = 0 initialMapGestureModeIsRotation = nil if let _ = displayLink { displayLink.invalidate() } } } // * // ****************************************************************************************** // ****************************************************************************************** // * // This is our main function. The display link calls it once every display frame. * // The moment the user let go of the map. var startRotateOut = NSTimeInterval(0) // After that, if there is still momentum left, the velocity is > 0. // The velocity of the rotation gesture in radians per second. private var remainingVelocityAfterUserInteractionEnded = CGFloat(0) // We need some values from the last frame private var prevHeading = CLLocationDirection() private var prevRotationInRadian = CGFloat(0) private var prevTime = NSTimeInterval(0) // The momentum gets slower ower time private var currentlyRemainingVelocity = CGFloat(0) func refreshCompassHeading(sender: AnyObject) { // If the gesture mode is not determinated or user is adjusting pitch // we do obviously nothing here. :) if initialMapGestureModeIsRotation == nil || !initialMapGestureModeIsRotation! { return } let rotationInRadian : CGFloat if remainingVelocityAfterUserInteractionEnded == 0 { // This is the normal case, when the map is beeing rotated. rotationInRadian = rotationGestureRecognizer.rotation } else { // velocity is > 0 or < 0. // This is the case when the user ended the gesture and there is // still some momentum left. let currentTime = NSDate.timeIntervalSinceReferenceDate() let deltaTime = currentTime - prevTime // Calculate new remaining velocity here. // This is only very empiric and leaves room for improvement. // For instance I noticed that in the middle of the translation // the needle rotates a bid faster than the map. let SLOW_DOWN_FACTOR : CGFloat = 1.87 let elapsedTime = currentTime - startRotateOut // Mathematicians, the next line is for you to play. currentlyRemainingVelocity -= currentlyRemainingVelocity * CGFloat(elapsedTime)/SLOW_DOWN_FACTOR let rotationInRadianSinceLastFrame = currentlyRemainingVelocity * CGFloat(deltaTime) rotationInRadian = prevRotationInRadian + rotationInRadianSinceLastFrame // Remember for the next frame. prevRotationInRadian = rotationInRadian prevTime = currentTime } // Convert radian to degree and get our long-desired new heading. let rotationInDegrees = Double(rotationInRadian * (180 / CGFloat(M_PI))) let newHeading = -mapView!.camera.heading + rotationInDegrees // No real difference? No expensive transform then. let difference = abs(newHeading - prevHeading) if difference < 0.001 { return } // Finally rotate the compass. arrowImageView.transform = CGAffineTransformMakeRotation(CGFloat(M_PI * newHeading) / 180.0) // Remember for the next frame. prevHeading = newHeading } // * // ****************************************************************************************** // As soon as this optional is set the initial mode is determined. // If it's true than the map is in rotation mode, // if false, the map is in 3D position adjust mode. private var initialMapGestureModeIsRotation : Bool? // ****************************************************************************************** // * // UIRotationGestureRecognizer * @IBAction func handleRotation(sender: UIRotationGestureRecognizer) { if (initialMapGestureModeIsRotation == nil) { initialMapGestureModeIsRotation = true } else if !initialMapGestureModeIsRotation! { // User is not in rotation mode. return } if sender.state == .Ended { if sender.velocity != 0 { // Velocity left after ending rotation gesture. Decelerate from remaining // momentum. This block is only called once. remainingVelocityAfterUserInteractionEnded = sender.velocity currentlyRemainingVelocity = remainingVelocityAfterUserInteractionEnded startRotateOut = NSDate.timeIntervalSinceReferenceDate() prevTime = startRotateOut prevRotationInRadian = rotationGestureRecognizer.rotation } } } // * // ****************************************************************************************** // * // Yes, there is also a SwypeGestureRecognizer, but the length for being recognized as * // is far too long. Recognizing a 2 finger swype up or down with a PanGestureRecognizer // yields better results. @IBAction func handleSwipe(sender: UIPanGestureRecognizer) { // After a certain altitude is reached, there is no pitch possible. // In this case the 3D perspective change does not work and the rotation is initialized. // Play with this one. let MAX_PITCH_ALTITUDE : Double = 100000 // Play with this one for best results detecting a swype. The 3D perspective change is // recognized quite quickly, thats the reason a swype recognizer here is of no use. let SWYPE_SENSITIVITY : CGFloat = 0.5 // play with this one if let _ = initialMapGestureModeIsRotation { // Gesture mode is already determined. // Swypes don't care us anymore. return } if mapView?.camera.altitude > MAX_PITCH_ALTITUDE { // Altitude is too high to adjust pitch. return } let panned = sender.translationInView(mapView) if fabs(panned.y) > SWYPE_SENSITIVITY { // Initial swype up or down. // Map gesture is most likely a 3D perspective correction. initialMapGestureModeIsRotation = false } } // * // ****************************************************************************************** // * @IBAction func pinchGestureRecognizer(sender: UIPinchGestureRecognizer) { // pinch is zoom. this always enables rotation mode. if (initialMapGestureModeIsRotation == nil) { initialMapGestureModeIsRotation = true // Initial pinch detected. This is normally a zoom // which goes in hand with a rotation. } } // * // ******************************************************************************************