iOS MKUserLocation 点的 10 个标题箭头
iOS 10 heading arrow for MKUserLocation dot
iOS10 中的地图应用程序现在在 MKUserLocation
MKAnnotationView
的顶部包含一个前进方向箭头。有什么方法可以将其添加到我自己的应用程序中的 MKMapView
?
编辑: 我很乐意手动执行此操作,但我不确定是否可行?我可以向地图添加注释并让它跟随用户的位置,包括动画移动吗?
是的,您可以手动执行此操作。
基本思想是使用 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
调用解决了这个小问题。
正确处理 coordinate
值更新已经在 MKPointAnnotation
、MKAnnotationView
和 MKMapView
类 中为我们实现,所以我们没有自己做。
我通过向 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
}
我也遇到过同样的问题(需要方向指示器而不需要地图旋转,类似于 Apple 地图应用程序)。遗憾的是,Apple 尚未提供 'blue icon for heading' API。
我创建了以下源自@alku83 实现的解决方案。
- 确保 class 符合 MKViewDelegate
添加委托方法以将蓝色箭头图标添加到地图位置点
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
if views.last?.annotation is MKUserLocation {
addHeadingView(toAnnotationView: views.last!)
}
}
添加创建'blue arrow icon'的方法。
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
}
}
将 var headingImageView: UIImageView?
添加到您的 class。这主要是transform/rotate蓝色箭头图像所需要的。
(在不同的 class/object 取决于你的架构)创建一个位置管理器实例,class 符合 CLLocationManagerDelegate
协议
lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
// Set up your manager properties here
manager.delegate = self
return manager
}()
确保您的位置管理器正在跟踪用户航向数据 locationManager.startUpdatingHeading()
并在适当的时候停止跟踪 locationManager.stopUpdatingHeading()
添加 var userHeading: CLLocationDirection?
将保存方向值
添加标题值改变时通知的委托方法,适当改变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)
}
现在在符合 MKMapViewDelegate 的 class 中,将方法添加到 'transform' 标题图像的方向
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)
}
}
我想知道为什么没有人提供 delegate
解决方案。它不依赖于 MKUserLocation
而是在大多数情况下使用@Dim_ov 提出的方法,即对 MKPointAnnotation
和 MKAnnotationView
进行子类化(恕我直言,这是最干净和最通用的方式).唯一的区别是观察者现在被替换为 delegate
方法。
创建 delegate
协议:
protocol HeadingDelegate : AnyObject {
func headingChanged(_ heading: CLLocationDirection)
}
创建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
}
}
创建 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))
})
}
}
考虑到您的视图控制器同时实现了 CLLocationManagerDelegate
和 MKMapViewDelegate
,剩下的事情就很少了(这里不提供完整的视图控制器代码):
// 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
) 的委托 属性 分配了注释视图 object。这将注释与其视图绑定在一起,这样每次修改标题 属性 时,都会调用视图的 headingChanged()
方法。
注意: didSet{}
和 willSet{}
属性 此处使用的观察者首次在 Swift 4 中引入。
iOS10 中的地图应用程序现在在 MKUserLocation
MKAnnotationView
的顶部包含一个前进方向箭头。有什么方法可以将其添加到我自己的应用程序中的 MKMapView
?
编辑: 我很乐意手动执行此操作,但我不确定是否可行?我可以向地图添加注释并让它跟随用户的位置,包括动画移动吗?
是的,您可以手动执行此操作。
基本思想是使用 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
调用解决了这个小问题。
正确处理 coordinate
值更新已经在 MKPointAnnotation
、MKAnnotationView
和 MKMapView
类 中为我们实现,所以我们没有自己做。
我通过向 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
}
我也遇到过同样的问题(需要方向指示器而不需要地图旋转,类似于 Apple 地图应用程序)。遗憾的是,Apple 尚未提供 'blue icon for heading' API。
我创建了以下源自@alku83 实现的解决方案。
- 确保 class 符合 MKViewDelegate
添加委托方法以将蓝色箭头图标添加到地图位置点
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { if views.last?.annotation is MKUserLocation { addHeadingView(toAnnotationView: views.last!) } }
添加创建'blue arrow icon'的方法。
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 } }
将
var headingImageView: UIImageView?
添加到您的 class。这主要是transform/rotate蓝色箭头图像所需要的。(在不同的 class/object 取决于你的架构)创建一个位置管理器实例,class 符合
CLLocationManagerDelegate
协议lazy var locationManager: CLLocationManager = { let manager = CLLocationManager() // Set up your manager properties here manager.delegate = self return manager }()
确保您的位置管理器正在跟踪用户航向数据
locationManager.startUpdatingHeading()
并在适当的时候停止跟踪locationManager.stopUpdatingHeading()
添加
var userHeading: CLLocationDirection?
将保存方向值添加标题值改变时通知的委托方法,适当改变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) }
现在在符合 MKMapViewDelegate 的 class 中,将方法添加到 'transform' 标题图像的方向
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) } }
我想知道为什么没有人提供 delegate
解决方案。它不依赖于 MKUserLocation
而是在大多数情况下使用@Dim_ov 提出的方法,即对 MKPointAnnotation
和 MKAnnotationView
进行子类化(恕我直言,这是最干净和最通用的方式).唯一的区别是观察者现在被替换为 delegate
方法。
创建
delegate
协议:protocol HeadingDelegate : AnyObject { func headingChanged(_ heading: CLLocationDirection) }
创建
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 } }
创建
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)) }) } }
考虑到您的视图控制器同时实现了
CLLocationManagerDelegate
和MKMapViewDelegate
,剩下的事情就很少了(这里不提供完整的视图控制器代码):// 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
) 的委托 属性 分配了注释视图 object。这将注释与其视图绑定在一起,这样每次修改标题 属性 时,都会调用视图的 headingChanged()
方法。
注意: didSet{}
和 willSet{}
属性 此处使用的观察者首次在 Swift 4 中引入。