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.
MKUserTrackingMode
->case followWithHeading
? developer.apple.com/reference/mapkit/mkusertrackingmode – Initiative