问题
The Maps app in iOS 10 now includes a heading direction arrow on top of the MKUserLocation
MKAnnotationView
. Is there some way I can add this to MKMapView
in my own apps?
Edit: I'd be happy to do this manually, but I'm not sure if it's possible? Can I add an annotation to the map and have it follow the user's location, including animated moves?
回答1:
I also experienced this same issue (needing an orientation indicator without having the map spin around, similar to the Apple Maps app). Unfortunately Apple has not yet made the 'blue icon for heading' API available.
I created the following solution derived from @alku83's implementation.
- Ensure the class conforms to MKViewDelegate
Add the delegate method to add a blue arrow icon to the maps location dot
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { if views.last?.annotation is MKUserLocation { addHeadingView(toAnnotationView: views.last!) } }
Add the method to create the '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 } }
Add
var headingImageView: UIImageView?
to your class. This is mainly needed to transform/rotate the blue arrow image.(In a different class/object depending on your architecture) Create a location manager instance, with the class conforming to
CLLocationManagerDelegate
protocollazy var locationManager: CLLocationManager = { let manager = CLLocationManager() // Set up your manager properties here manager.delegate = self return manager }()
Ensure your location manager is tracking user heading data
locationManager.startUpdatingHeading()
and that it stops tracking when appropriatelocationManager.stopUpdatingHeading()
Add
var userHeading: CLLocationDirection?
which will hold the orientation valueAdd the delegate method to be notified of when the heading values change, and change the userHeading value appropriately
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) }
Now in your class conforming to MKMapViewDelegate, add the method to 'transform' the orientation of the heading image
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) } }
回答2:
Yes, you can do this manually.
The basic idea is to track user's location with CLLocationManager
and use it's data for placing and rotating annotation view on the map.
Here is the code. I'm omitting certain things that are not directly related to the question (e.g. I'm assuming that user have already authorized your app for location access, etc.), so you'll probably want to modify this code a little bit
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)
}
}
}
Here we are doing basic setup of the map view and starting to track user's location and heading with the 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
}
Very simple MKPointAnnotation
subclass that is capable of storing heading direction. dynamic
keyword is the key thing here. It allows us to observe changes to the heading
property with KVO.
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
subclass that does the observation of the heading
property and then sets the appropriate rotation transform to it's subview (in my case it's just an image with the arrow. You can create more sophisticated annotation view and rotate only some part of it instead of the whole view.)
UIView.animate
is optional. It is added to make rotation smoother. CLLocationManager
is not capable of observing heading value 60 times per second, so when rotating fast, animation might be a little bit choppy. UIView.animate
call solves this tiny issue.
Proper handling of coordinate
value updates is already implemented in MKPointAnnotation
, MKAnnotationView
and MKMapView
classes for us, so we don't have to do it ourselves.
回答3:
I solved this by adding a subview to the MKUserLocation
annotationView, like so
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
}
回答4:
I wonder why no one offered a delegate
solution. It does not rely on MKUserLocation
but rather uses the approach proposed by @Dim_ov for the most part i.e. subclassing both MKPointAnnotation
and MKAnnotationView
(the cleanest and the most generic way IMHO). The only difference is that the observer is now replaced with a delegate
method.
Create the
delegate
protocol:protocol HeadingDelegate : AnyObject { func headingChanged(_ heading: CLLocationDirection) }
Create
MKPointAnnotation
subclass that notifies the delegate. TheheadingDelegate
property will be assigned externally from the view controller and triggered every time theheading
property changes: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 } }
Create
MKAnnotationView
subclass that implements the delegate: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)) }) } }
Considering that your view controller implements both
CLLocationManagerDelegate
andMKMapViewDelegate
there is very little left to do (not providing full view controller code here):// 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 }
The most important part is where the delegate property of the annotation (headingDelegate
) is assigned with the annotation view object. This binds the annotation with it's view such that every time the heading property is modified the view's headingChanged()
method is called.
NOTE: didSet{}
and willSet{}
property observers used here were first introduced in Swift 4.
来源:https://stackoverflow.com/questions/39762732/ios-10-heading-arrow-for-mkuserlocation-dot