MKMapView constantly monitor heading

后端 未结 4 1041
耶瑟儿~
耶瑟儿~ 2021-01-16 00:44

I\'m rendering some content in a layer that sits on top of my MKMapView. The whole thing works great with the exception of rotation. When a user rotates the map

相关标签:
4条回答
  • 2021-01-16 01:02

    Instead of passing a nil context, pass a value to compare with on you KVO observer, like this:

    static void *CameraContext= &CameraContext;

        // Somewhere in setup
        [self.mapView.camera addObserver:self forKeyPath:@"heading" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:CameraContext];
    
    
    // KVO Callback
    -(void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary *)change
                          context:(void *)context{
    
     if (context == CameraContext) {
        if([keyPath isEqualToString:@"heading"]){
            // New value
        }
      }
    }
    
    0 讨论(0)
  • 2021-01-16 01:06

    Check this answer, which you could adapt (using CADisplayLink):

    MapView detect scrolling

    0 讨论(0)
  • 2021-01-16 01:06

    There seem to exist indeed no way to track the simply read the current heading while rotation the map. Since I just implemented a compass view that rotates with the map, I want to share my knowledge with you.

    I explicitly invite you to refine this answer. Since I have a deadline, I'm satisfied as it is now (before that, the compass was only set in the moment the map stopped to rotate) but there is room for improvement and finetuning.

    I uploaded a sample project here: MapRotation Sample Project

    Okay, let's start. Since I assume you all use Storyboards nowadays, drag a few gesture recognizers to the map. (Those who don't surely knows how to convert these steps into written lines.)

    To detect map rotation, zoom and 3D angle we need a rotation, a pan and a pinch gesture recognizer.

    Disable "Delays touches ended" for the Rotation Gesture Recognizer...

    ... and increase "Touches" to 2 for the Pan Gesture Recognizer.

    Set the delegate of these 3 to the containing view controller.

    Drag for all 3 gesture recognizers the Referencing Outlet Collections to the MapView and select "gestureRecognizers"

    Now Ctrl-drag the rotation gesture recognizer to the implementation as Outlet like this:

    @IBOutlet var rotationGestureRecognizer: UIRotationGestureRecognizer!
    

    and all 3 recognizers as IBAction:

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

    Yes, I named the pan gesture "handleSwype". It's explained below. :)

    Listed below the complete code for the controller that of course also has to implement the MKMapViewDelegate protocol. I tried to be very detailed in the comments.

    // 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.
        }
    }
    //                                                                                          *
    // ******************************************************************************************
    
    0 讨论(0)
  • 2021-01-16 01:17

    I've seen similar behavior in a MapKit program for OS X. I'm using the <MKMapViewDelegate> call mapView:regionDidChangeAnimated: instead of a KVO notification on changes to heading, but I'm still only seeing the call at the end of rotations.

    I just tried implementing mapView:regionWillChangeAnimated:. That does in fact get called at the beginning of rotations. Perhaps you could begin polling the region upon receipt of mapView:regionWillChangeAnimated:, cease polling on mapView:regionDidChangeAnimated:, and in between do whatever critical updates you need during the rotations.

    0 讨论(0)
提交回复
热议问题