【问题标题】:iOS 10 heading arrow for MKUserLocation dotMKUserLocation 点的 iOS 10 标题箭头
【发布时间】:2016-09-29 06:16:50
【问题描述】:

iOS 10 中的地图应用现在在 MKUserLocation MKAnnotationView 顶部包含一个方向箭头。有什么方法可以在我自己的应用程序中将其添加到MKMapView

编辑:我很乐意手动执行此操作,但我不确定是否可行?我可以在地图上添加注释并让它跟随用户的位置,包括动画移动吗?

【问题讨论】:

  • MKUserTrackingMode -> case followWithHeading 怎么样? developer.apple.com/reference/mapkit/mkusertrackingmode
  • 您好,感谢您的输入,但这并不能像上面那样启用图形。
  • 其实更重要的是,我想显示一个航向指示器,而不是跟随用户位置的地图。

标签: ios mkmapview ios10 mkuserlocation


【解决方案1】:

我也遇到了同样的问题(需要一个方向指示器而不让地图旋转,类似于 Apple 地图应用程序)。不幸的是,Apple 还没有提供“标题的蓝色图标”API。

我从@alku83 的实现中创建了以下解决方案。

  1. 确保类符合 MKViewDelegate
  2. 添加委托方法以将蓝色箭头图标添加到地图位置点

    func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
        if views.last?.annotation is MKUserLocation {
            addHeadingView(toAnnotationView: views.last!)
        }
    }
    
  3. 添加创建“蓝色箭头图标”的方法。

    func addHeadingView(toAnnotationView annotationView: MKAnnotationView) {
        if headingImageView == nil {
            let image = #YOUR BLUE ARROW ICON#
            headingImageView = UIImageView(image: image)
            headingImageView!.frame = CGRect(x: (annotationView.frame.size.width - image.size.width)/2, y: (annotationView.frame.size.height - image.size.height)/2, width: image.size.width, height: image.size.height)
            annotationView.insertSubview(headingImageView!, at: 0)
            headingImageView!.isHidden = true
         }
    }
    
  4. var headingImageView: UIImageView? 添加到您的班级。这主要用于变换/旋转蓝色箭头图像。

  5. (在不同的类/对象中,具体取决于您的架构)创建一个位置管理器实例,该类符合CLLocationManagerDelegate 协议

    lazy var locationManager: CLLocationManager = {
        let manager = CLLocationManager()
        // Set up your manager properties here
        manager.delegate = self
        return manager
    }()
    
  6. 确保您的位置管理器正在跟踪用户标题数据locationManager.startUpdatingHeading(),并在适当的时候停止跟踪locationManager.stopUpdatingHeading()

  7. 添加var userHeading: CLLocationDirection?,它将保存方向值

  8. 添加标题值变化时通知的委托方法,并适当更改userHeading值

    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
         if newHeading.headingAccuracy < 0 { return }
    
         let heading = newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading
         userHeading = heading
         NotificationCenter.default.post(name: Notification.Name(rawValue: #YOUR KEY#), object: self, userInfo: nil)
        }
    
  9. 现在在符合 MKMapViewDelegate 的类中,添加方法来“转换”标题图像的方向

       func updateHeadingRotation() {
            if let heading = # YOUR locationManager instance#,
                let headingImageView = headingImageView {
    
                headingImageView.isHidden = false
                let rotation = CGFloat(heading/180 * Double.pi)
                headingImageView.transform = CGAffineTransform(rotationAngle: rotation)
            }
        }
    

【讨论】:

  • 太棒了!非常感谢!
  • 为什么我们需要发布通知#YOUR KEY#?航向变化时调用“updateHeadingRotation”还不够吗?
  • @Satyam 是的,你可以那样做。
  • 谢谢!您可以使用 arcPath 来模拟 iOS 14,而不是使用图像视图:gist.github.com/nmondollot/6cda942b9c4f130b91a27fac7ee7d9f9 注意:您需要使用 didUpdateHeading 委托方法来获取航向精度。
【解决方案2】:

是的,您可以手动执行此操作。

基本思想是使用CLLocationManager 跟踪用户的位置,并使用它的数据在地图上放置和旋转注释视图。

这里是代码。我省略了与问题不直接相关的某些内容(例如,我假设用户已经授权您的应用进行位置访问等),因此您可能需要稍微修改此代码

ViewController.swift

import UIKit
import MapKit

class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
    @IBOutlet var mapView: MKMapView!
    lazy var locationManager: CLLocationManager = {
        let manager = CLLocationManager()
        manager.delegate = self
        return manager
    }()

    var userLocationAnnotation: UserLocationAnnotation!

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation

        locationManager.startUpdatingHeading()
        locationManager.startUpdatingLocation()

        userLocationAnnotation = UserLocationAnnotation(withCoordinate: CLLocationCoordinate2D(), heading: 0.0)

        mapView.addAnnotation(userLocationAnnotation)
    }

    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        userLocationAnnotation.heading = newHeading.trueHeading
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        userLocationAnnotation.coordinate = locations.last!.coordinate
    }

    public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        if let annotation = annotation as? UserLocationAnnotation {
            let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "UserLocationAnnotationView") ?? UserLocationAnnotationView(annotation: annotation, reuseIdentifier: "UserLocationAnnotationView")
            return annotationView
        } else {
            return MKPinAnnotationView(annotation: annotation, reuseIdentifier: nil)
        }
    }

}

在这里,我们正在对地图视图进行基本设置,并开始使用CLLocationManager 跟踪用户的位置和航向。

UserLocationAnnotation.swift

import UIKit
import MapKit

class UserLocationAnnotation: MKPointAnnotation {
    public init(withCoordinate coordinate: CLLocationCoordinate2D, heading: CLLocationDirection) {
        self.heading = heading

        super.init()
        self.coordinate = coordinate
    }

    dynamic public var heading: CLLocationDirection
}

非常简单的MKPointAnnotation 子类,能够存储航向方向。 dynamic 关键字是这里的关键。它允许我们使用 KVO 观察 heading 属性的变化。

UserLocationAnnotationView.swift

import UIKit
import MapKit

class UserLocationAnnotationView: MKAnnotationView {

    var arrowImageView: UIImageView!

    private var kvoContext: UInt8 = 13

    override public init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

        arrowImageView = UIImageView(image: #imageLiteral(resourceName: "Black_Arrow_Up.svg"))
        addSubview(arrowImageView)
        setupObserver()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        arrowImageView = UIImageView(image: #imageLiteral(resourceName: "Black_Arrow_Up.svg"))
        addSubview(arrowImageView)
        setupObserver()
    }

    func setupObserver() {
        (annotation as? UserLocationAnnotation)?.addObserver(self, forKeyPath: "heading", options: [.initial, .new], context: &kvoContext)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &kvoContext {
            let userLocationAnnotation = annotation as! UserLocationAnnotation
            UIView.animate(withDuration: 0.2, animations: { [unowned self] in
                self.arrowImageView.transform = CGAffineTransform(rotationAngle: CGFloat(userLocationAnnotation.heading / 180 * M_PI))
            })
        }
    }

    deinit {
        (annotation as? UserLocationAnnotation)?.removeObserver(self, forKeyPath: "heading")
    }
}

MKAnnotationView 子类,它观察heading 属性,然后为其子视图设置适当的旋转变换(在我的情况下,它只是一个带箭头的图像。您可以创建更复杂的注释视图并仅旋转一些它的一部分而不是整个视图。)

UIView.animate 是可选的。添加它是为了使旋转更平滑。 CLLocationManager 无法每秒观察 60 次航向值,因此在快速旋转时,动画可能会有点不稳定。 UIView.animate call 解决了这个小问题。

coordinate 值更新的正确处理已经在 MKPointAnnotationMKAnnotationViewMKMapView 类中为我们实现了,所以我们不必自己做。

【讨论】:

  • 感谢您的输入 - 这与我最终使用的方法不同,但似乎这也应该有效。我唯一担心的是,当某些事件发生时,附加注释和 MKUserLocation 注释之间会断开连接,例如。位置发生变化,MKUserLocation 注释动画到新位置。
  • 您没有使用从 didUpdateHeading 函数获得的标题,并且在观察者中您使用用户位置标题(即蓝点)计算旋转。对吧??
  • @AdeelUrRehman,没有。我确实使用来自didUpdateHeading 的标题值。我将它保存到自定义注释的heading 属性中,并在注释视图中观察该属性的变化。所以正是didUpdateHeading 调用触发了UserLocationAnnotationView 中的轮换。而用于旋转角度的值正是didUpdateHeading中接收到的值。
【解决方案3】:

我通过向MKUserLocation annotationView 添加一个子视图来解决这个问题,就像这样

func mapView(mapView: MKMapView, didAddAnnotationViews views: [MKAnnotationView]) {
if annotationView.annotation is MKUserLocation {
    addHeadingViewToAnnotationView(annotationView)
    }
}

func addHeadingViewToAnnotationView(annotationView: MKAnnotationView) {
    if headingImageView == nil {
        if let image = UIImage(named: "icon-location-heading-arrow") {
            let headingImageView = UIImageView()
            headingImageView.image = image
            headingImageView.frame = CGRectMake((annotationView.frame.size.width - image.size.width)/2, (annotationView.frame.size.height - image.size.height)/2, image.size.width, image.size.height)
            self.headingImageView = headingImageView
        }
    }

    headingImageView?.removeFromSuperview()
    if let headingImageView = headingImageView {
        annotationView.insertSubview(headingImageView, atIndex: 0)
    }

    //use CoreLocation to monitor heading here, and rotate headingImageView as required
}

【讨论】:

  • 能否请您添加更多有关CoreLocation 的详细代码来监控航向?
  • @AmritTiwari 这个答案应该涵盖大部分内容stackoverflow.com/a/40808645/334982
  • 你能把它贴在这里吗?
  • @AmritTiwari 这是同一页面上评价最高的答案。向上滚动一点...
【解决方案4】:

我想知道为什么没有人提供delegate 解决方案。它不依赖MKUserLocation,而是在很大程度上使用@Dim_ov 提出的方法,即继承MKPointAnnotationMKAnnotationView(恕我直言,最干净和最通用的方式)。唯一的区别是观察者现在被替换为delegate 方法。

  1. 创建delegate 协议:

    protocol HeadingDelegate : AnyObject {
        func headingChanged(_ heading: CLLocationDirection)
    }
    
  2. 创建通知代理的MKPointAnnotation 子类。 headingDelegate 属性将从视图控制器外部分配,并在每次 heading 属性更改时触发:

    class Annotation : MKPointAnnotation {
        weak var headingDelegate: HeadingDelegate?
        var heading: CLLocationDirection {
            didSet {
                headingDelegate?.headingChanged(heading)
            }
        }
    
        init(_ coordinate: CLLocationCoordinate2D, _ heading: CLLocationDirection) {
            self.heading = heading
            super.init()
            self.coordinate = coordinate
        }
    }
    
  3. 创建实现委托的MKAnnotationView子类:

    class AnnotationView : MKAnnotationView , HeadingDelegate {
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
            super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        }
    
        func headingChanged(_ heading: CLLocationDirection) {
            // For simplicity the affine transform is done on the view itself
            UIView.animate(withDuration: 0.1, animations: { [unowned self] in
                self.transform = CGAffineTransform(rotationAngle: CGFloat(heading / 180 * .pi))
            })
        }
    }
    
  4. 考虑到您的视图控制器同时实现了CLLocationManagerDelegateMKMapViewDelegate,剩下的工作就很少了(这里不提供完整的视图控制器代码):

        // Delegate method of the CLLocationManager
        func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
            userAnnotation.heading = newHeading.trueHeading
        }
    
        // Delegate method of the MKMapView
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {        
            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: NSStringFromClass(Annotation.self))
            if (annotationView == nil) {
                annotationView = AnnotationView(annotation: annotation, reuseIdentifier: NSStringFromClass(Annotation.self))
            } else {
                annotationView!.annotation = annotation
            }
    
            if let annotation = annotation as? Annotation {
                annotation.headingDelegate = annotationView as? HeadingDelegate
                annotationView!.image = /* arrow image */
            }
    
            return annotationView
        }
    

最重要的部分是注释的委托属性(headingDelegate)与注释视图对象一起分配。这会将注解与其视图绑定,这样每次修改标题属性时,都会调用视图的headingChanged() 方法。

注意:这里使用的didSet{}willSet{} 属性观察器最初是在 Swift 4 中引入的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-26
    • 1970-01-01
    相关资源
    最近更新 更多