MKMapView不断地监视标题

我正在渲染一个位于我的MKMapView之上的图层中的一些内容。 除了旋转,整个事情都很好。 当用户旋转地图时,我需要能够旋转我在自己的图层中呈现的内容。

我发现的标准答案是使用:

 NSLog(@"heading: %f", self.mapView.camera.heading"); 

问题在于标题variables的内容仅在捏放/旋转手势结束时才更新,而不是在手势期间更新。 我需要更频繁的更新。

mapView本身没有标题属性。

我想也许使用这样的KVO

  // Somewhere in setup [self.mapView.camera addObserver:self forKeyPath:@"heading" options:NSKeyValueObservingOptionNew context:NULL]; // KVO Callback -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ if([keyPath isEqualToString:@"heading"]){ // New value } } 

然而KVO的听众从来不会发火,这并不奇怪。

有没有我忽略的方法?

检查您可以调整的答案(使用CADisplayLink ):

MapView检测滚动

在旋转地图的时候,似乎没有办法跟踪简单地读取当前的标题。 由于我刚刚实现了一个随地图旋转的罗盘视图,我想与你分享我的知识。

我明确地邀请你来完善这个答案。 由于我有期限,所以我现在很满意(在此之前,指南针只是在地图停止旋转的那一刻),但是还有改进和调整的余地。

我在这里上传了一个示例项目: MapRotation示例项目

好的,我们开始吧。 由于我假设你们现在都使用故事板,所以将一些手势识别器拖到地图上。 (那些谁不知道如何将这些步骤转换成书面的线。)

要检测地图旋转,缩放和三维angular度,我们需要一个旋转,平移和捏手势识别器。 在MapView上拖动手势识别器

为旋转手势识别器禁用“延迟触摸结束”… 为旋转手势识别器禁用“延迟触摸结束”

…并将泛触手势识别器的“触摸”增加到2。 将Pan手势识别器的“触摸”增加到2

将这3个代理设置为包含视图控制器。 按住Ctrl键拖动到包含的视图控制器... ...并设置代表。

将所有3个手势识别器的引用出口集合拖动到MapView,然后select“gestureRecognizers”

在这里输入图像说明

现在,按下Ctrl键将旋转手势识别器拖放到Outlet的实现中,如下所示:

 @IBOutlet var rotationGestureRecognizer: UIRotationGestureRecognizer! 

和所有3个识别器作为IBAction:

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

是的,我将平移手势命名为“handleSwype”。 这在下面解释。 🙂

下面列出了控制器的完整代码,当然也必须实现MKMapViewDelegate协议。 我试图在评论中非常详细。

 // 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. } } // * // ****************************************************************************************** 

而不是传递一个nil上下文,传递一个值来比较你的KVO观察者,就像这样:

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 } } } 

我已经看到在OS X的MapKit程序中类似的行为。我正在使用<MKMapViewDelegate>调用mapView:regionDidChangeAnimated:而不是一个KVO通知更改heading ,但我仍然只看到在旋转。

我只是试图实现mapView:regionWillChangeAnimated: 事实上在轮换开始时就会被调用。 也许你可以在收到mapView:regionWillChangeAnimated:时停止对mapView:regionDidChangeAnimated:轮询,并在两者之间进行旋转期间所需的关键更新。