Ios 轨迹贴图视图旋转

Ios 轨迹贴图视图旋转,ios,rotation,mapkit,Ios,Rotation,Mapkit,我有一个MapKitView,它有一个指向某个方向的注释。我的问题是,当用户使用两个手指旋转地图时,或者如果地图旋转以跟踪用户的标题,则需要旋转我的符号(因为它们与屏幕对齐,所以不需要旋转) 我知道我可以通过与地图摄影机标题相反的方向旋转符号。 我知道在这种情况下,可以通知我用户标题的更改以旋转注释 我的问题是,由于用户以交互方式旋转地图,我找不到跟踪地图旋转的方法 我可以跟踪贴图区域的开始和结束更改,但不能跟踪两者之间的更改。 我试着用KVO的相机的轴承,但我没有得到任何东西。 我试图查找系统

我有一个MapKitView,它有一个指向某个方向的注释。我的问题是,当用户使用两个手指旋转地图时,或者如果地图旋转以跟踪用户的标题,则需要旋转我的符号(因为它们与屏幕对齐,所以不需要旋转)

我知道我可以通过与地图摄影机标题相反的方向旋转符号。 我知道在这种情况下,可以通知我用户标题的更改以旋转注释

我的问题是,由于用户以交互方式旋转地图,我找不到跟踪地图旋转的方法

我可以跟踪贴图区域的开始和结束更改,但不能跟踪两者之间的更改。 我试着用KVO的相机的轴承,但我没有得到任何东西。 我试图查找系统发送的通知,但还是没有


有人对如何可靠地跟踪当前地图旋转有什么建议吗?

您可以尝试创建一个
CADisplayLink
,该链接将在屏幕刷新时触发一个选择器,该选择器足以与MapKit动画的每一帧同步。在每个过程中,检查方向值并更新注释视图

似乎确实没有办法在旋转地图的同时简单地读取当前标题来跟踪地图。因为我刚刚实现了一个随地图旋转的指南针视图,所以我想与大家分享我的知识

我明确邀请你们完善这个答案。由于我有一个截止日期,我很满意现在的情况(在此之前,罗盘只是在地图停止旋转的那一刻设置的),但是还有改进和微调的空间

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

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

要检测地图旋转、缩放和3D角度,我们需要旋转、平移和收缩手势识别器。

禁用旋转手势识别器的“延迟结束”。。。

。。。并将平移手势识别器的“触碰”增加到2。

将这3个视图的委托设置为包含的视图控制器。

将所有3个手势识别器的引用插座集合拖到地图视图,然后选择“手势识别器”

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

以及所有3个识别器作为iAction:

是的,我把这个平移动作命名为“无柄Wype”。下面将对此进行解释。:)

下面列出了控制器的完整代码,当然也必须实现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.
    }
}
//                                                                                          *
// ******************************************************************************************
//compassView是容器视图,
//arrowImageView是将要旋转的箭头
@IBVAR compassView:UIView!
var arrowImageView=UIImageView(图像:UIImage(名为:“指南针”)!)
重写func viewDidLoad(){
super.viewDidLoad()
compassView.addSubview(箭头图像视图)
}
// ******************************************************************************************
//                                                                                          *
//辅助对象:在地图视图更改时检测*
private func mapViewRegionDidChangeFromUserInteraction()->Bool{
让视图=地图视图!。子视图[0]
//查看手势识别器以确定此区域
//变化来自用户交互
如果让gestureRecognizers=view.gestureRecognizers{
用于手势识别器中的识别器{
如果(recognizer.state==UIgestureCongnizerState.Start)||
recognizer.state==UIGestureRecognizerState.Ended){
返回真值
}
}
}
返回错误
}
//                                                                                          *
// ******************************************************************************************
// ******************************************************************************************
//                                                                                          *
//助手:需要允许同时识别MapView中的手势*
func gestureRecognizer(uquo:UIGestureRecognizer,
应使用手势识别器同时识别:UIGestureRecognizer)->Bool{
返回真值
}
//                                                                                          *
// ******************************************************************************************
// ******************************************************************************************
//                                                                                          *
//助手:使用CADisplayLink在屏幕刷新时触发选择器,以与每个屏幕同步*
//MapKit动画的帧
私有变量显示链接:CADisplayLink!
func setUpDisplayLink(){
displayLink=CADisplayLink(目标:self,选择器:“refreshCompassHeading:”)
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(),forMode:NSRunLoopCommonModes)
}
//                                                                                          *
// ******************************************************************************************
// ******************************************************************************************
//                                                                                          *
//检测用户是否开始与地图交互*
私有变量mapChangedFromUserInteraction=false
func地图视图(地图视图:MKMapView,区域将更改动画:Bool){
mapChangedFromUserInteraction=mapViewRegionDidChangeFromUserInteraction()
if(mapChangedFromUserInteraction){
//地图交互。设置CADisplayLink。
@IBAction func handleRotation(sender: UIRotationGestureRecognizer) {
    ...
}

@IBAction func handleSwipe(sender: UIPanGestureRecognizer) {
    ...
}

@IBAction func pinchGestureRecognizer(sender: UIPinchGestureRecognizer) {
    ...
}
// 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.
    }
}
//                                                                                          *
// ******************************************************************************************
class MyMap : MKMapView {

}
class MyMap : MKMapView {

  var mapContainerView : UIView?

  init() {
      ...
      self.mapContainerView = self.findViewOfType("MKScrollContainerView", inView: self)
      ...
  }

  func findViewOfType(type: String, 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
      }
  }

}
 cosA  sinA  0
-sinA  cosA  0
  0     0    1
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
    }
}

...

}
@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 listener : 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.listener?.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
        }
    }


}
protocol MyMapViewRotationDelegate:class {
    func myMapView(mapView: MyMapView, didRotateAtAngle angle:CGFloat)
}

class MyMapView: MKMapView {
    private var mapContainerView:UIView?
    weak var rotationDelegate:MyMapViewRotationDelegate?
}
private func findViewOfType(type: String, 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.classForCoder == NSClassFromString("MKScrollContainerView") {
                    return v
                }

                if let inSubviews = self.findViewOfType(type: type, inView: v) {
                    return inSubviews
                }
            }
            return nil
        } else {
            return nil
        }
    }

override func awakeFromNib() {
        super.awakeFromNib()

        if let scrollContainerView = findViewOfType(type: "MKScrollContainerView", inView: self) {
            mapContainerView = scrollContainerView
            mapContainerView!.layer.addObserver(self, forKeyPath: #keyPath(transform), options: [.new, .old], context: nil)
        }
    }
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {        
        guard keyPath == #keyPath(transform) else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }
            guard let layer = object as? CALayer else {
                return
            }

            if let rotationRadians = layer.value(forKeyPath: "transform.rotation.z") as? CGFloat {
                var angle = rotationRadians / .pi * 180 //convert to degrees
                if  angle < 0 {
                    angle = 360 + angle
                }

                if let rotationDelegate = rotationDelegate {
                    rotationDelegate.myMapView(mapView: self, didRotateAtAngle: angle)
                }
            }
        }
deinit {
        mapContainerView?.layer.removeObserver(self, forKeyPath: #keyPath(transform))
    }