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?
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 protocol
lazy 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 appropriate locationManager.stopUpdatingHeading()
Add var userHeading: CLLocationDirection? which will hold the orientation value
Add 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)
}
}
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.
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
}
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. The headingDelegate property will be assigned externally from the view controller and triggered every time the heading 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 and MKMapViewDelegate 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.
Related
I have an array of latitudes and another array of longitudes that I add to an array of type CLLocationCoordinate2D. I then use the new array to annotate multiple points on the map. Some, or most, or maybe even all of the annotations are displaying on the map but as I zoom in (yes, zoom IN), some of the annotations disappear, then come back, or dont. Any ideas on how to keep them all visible? This is behavior I would expect while zooming out, not in.
Here is the code i'm using for what i've described above.
import UIKit
import MapKit
import CoreLocation
class MultiMapVC: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var multiEventMap: MKMapView!
var latDouble = Double()
var longDouble = Double()
let manager = CLLocationManager()
var receivedArrayOfLats = [Double]()
var receivedArrayOfLongs = [Double]()
var locations = [CLLocationCoordinate2D]()
func locationManager(_ manager: CLLocationManager, didUpdateLocations uLocation: [CLLocation]) {
let userLocation = uLocation[0]
let span:MKCoordinateSpan = MKCoordinateSpanMake(0.3, 0.3)
let usersLocation = userLocation.coordinate
let region:MKCoordinateRegion = MKCoordinateRegionMake(usersLocation, span)
multiEventMap.setRegion(region, animated: true)
manager.distanceFilter = 1000
self.multiEventMap.showsUserLocation = true
}
func multiPoint() {
var coordinateArray: [CLLocationCoordinate2D] = []
print ("Received Longitude Count = \(receivedArrayOfLongs.count)")
print ("Received Latitude Count = \(receivedArrayOfLats.count)")
if receivedArrayOfLats.count == receivedArrayOfLongs.count {
for i in 0 ..< receivedArrayOfLats.count {
let eventLocation = CLLocationCoordinate2DMake(receivedArrayOfLats[i], receivedArrayOfLongs[i])
coordinateArray.append(eventLocation)
print (coordinateArray.count)
}
}
for events in coordinateArray {
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: events.latitude, longitude: events.longitude)
multiEventMap.addAnnotation(annotation)
}
}
override func viewDidLoad() {
super.viewDidLoad()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
multiPoint()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
multiEventMap.removeFromSuperview()
self.multiEventMap = nil
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
NiltiakSivad's solution works but it reverts to the old iOS 10 look. If you want to keep the new iOS 11 balloon markers for iOS 11 and use the old pin look only for older iOS versions then you can implement the delegate method as below:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let reuseIdentifier = "annotationView"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
if #available(iOS 11.0, *) {
if view == nil {
view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
view?.displayPriority = .required
} else {
if view == nil {
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
}
view?.annotation = annotation
view?.canShowCallout = true
return view
}
The accepted answer from Leszek Szary is correct.
But there is some fineprint. Sometimes MKMarkerAnnotationViews are not rendered, even if
view.displayPriority = .required
is set.
What you are seeing is a combination of different rules.
MKAnnotationViews are rendered from top to bottom of the map. (It doesn't matter where north is).
If MapKit decides to draw overlapping MKAnnotationViews, then the MKAnnotationView nearer to the bottom is drawn on top (because it's drawn later)
Not only MKAnnotationViews, also titles rendered below MKMArkerAnnotationViews need space. The rendering of those titles is influenced by markerView.titleVisibility. If markerView.titleVisibility is set to .visible (instead of the default .adaptive), then this title is stronger than a MarkerAnnotationView that is rendered later, even if the later MarkerAnnotationView has a displayPriority = .required. The MarkerAnnotationView nearer to the bottom is not rendered.
This even happens if the MarkerAnnotationView nearer to the top has a low displayPriority. So a MarkerAnnotationView with low displayPriority and .titleVisibility = .visible can make a MarkerAnnotationView nearer to the bottom with displayPriority = .required disappear.
I am not aware of a documentation of this behaviour. This is the result of my experiments with iOS 12. My description is sipmplified.
I was experiencing a similar issue. My best guess is that it has something to do with how iOS 11 detects pin collisions. Implementing a custom annotation view or reverting to use the iOS 10 pin fixed the problem for me.
For example, implementing the following should fix your code:
class MultiMapVC: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? MKPointAnnotation else { return nil }
let identifier = "pin-marker"
var view: MKAnnotationView
if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView {
dequeuedView.annotation = annotation
view = dequeuedView
} else {
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
}
return view
}
}
If this doesn't work, there is a displayPriority property that is worth looking into as it is responsible for helping to determine when pins should be hidden/shown at different zoom levels. More info at https://developer.apple.com/documentation/mapkit/mkannotationview/2867298-displaypriority
Hope this helps.
I was setting annotationView.displayPriority = .required only when the MKMarkAnnotationView was first allocated. Normally thats all you should need to do, but setting it each time the cell was reused fixed the issue for me.
I was seeing a similar problem with Xcode 10, and iOS 12+ as my deployment target. A post from an Apple staffer (https://forums.developer.apple.com/thread/92839) recommends toggling the .isHidden property on the dequeued marker. That has improved things but has not completely solved the problem.
result?.isHidden = true
result?.isHidden = false
return result
I have a UIView that integrate a scrollview. Inside the scrollview I have a map where a user need to move a pin. I'm gonna reuse the location of pin later.
I'm setting strong reference and setting delegate ( both design and code) for the map. I'm able to see the pin created but to have custom icon and possibility to set it draggable, I have implement the mapView( viewFor MKAnnotation
This seems to be never called and I'm not getting why.
I also try to get another delegate as didfinishloading to understand if problem is related to the mkannotation but also this one is never been called.
import Foundation
import Firebase
import MapKit
import UIKit
public class Setup: UIViewController,CLLocationManagerDelegate,MKMapViewDelegate{
var ChargePointID = ""
#IBOutlet weak var chargepointIDLabel: UILabel!
let locationManager = CLLocationManager()
var pin: customPin!
//setting up variables
//pricing textfield and slider
#IBOutlet weak var priceLabel: UITextField!
#IBOutlet weak var priceSlider: UISlider!
#IBOutlet var map: MKMapView!
override public func viewDidLoad() {
super.viewDidLoad()
chargepointIDLabel.text = ChargePointID
self.map.delegate = self
self.map.mapType = .hybrid
// Ask for Authorisation from the User.
self.locationManager.requestAlwaysAuthorization()
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
//create liocation relative to user
let location = CLLocationCoordinate2D(latitude: (locationManager.location?.coordinate.latitude)!, longitude: (locationManager.location?.coordinate.longitude)!)
//centering map to user location
let region = MKCoordinateRegion(center: location, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))
self.map.setRegion(region, animated: true)
//creating a pin of type custompin
pin = customPin(pinTitle: ChargePointID, pinSubTitle: "", location: location)
map.addAnnotation(pin)
}
private func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
print("locations = \(locValue.latitude) \(locValue.longitude)")
}
private func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
print("annotation title == \(String(describing: view.annotation?.title!))")
}
private func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
print("ok")
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "customannotation")
annotationView.image = UIImage(named:"setuppin")
annotationView.canShowCallout = true
return annotationView
}
func mapViewDidFinishLoadingMap(_ mapView: MKMapView) {
print(" map has finished")
}
private func map(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
print("ok")
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "customannotation")
annotationView.image = UIImage(named:"setuppin")
annotationView.canShowCallout = true
return annotationView
}
}
class customPin: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
init(pinTitle:String, pinSubTitle:String, location:CLLocationCoordinate2D) {
self.title = pinTitle
self.subtitle = pinSubTitle
self.coordinate = location
}
}
private is not necessary. Replace
private func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
with
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
this was cause Xcode was raising this warning
instance method 'mapView(:viewFor:)' nearly matches optional requirement 'mapView(:viewFor:)' of protocol 'MKMapViewDelegate'
and suggest make it private as fix. btw even without private not raise anything
DAMN!!!!!
I solved it.... the warning was cause my class was declared ad public and delegate need to be too.
I removed the public statement from my class and everything start to work .
so problem in end was main class declared as public !
I'm trying to set a custom callout view with a xib I created however it doesn't show up.
My xib LocationInfo looks like this
I've created a custom uiview class for the view in my xib to set a background image (not sure if this works since I haven't been able to show the xib)
import Foundation
import UIKit
import MapKit
class AddressView: MKPinAnnotationView{
override func draw(_ rect: CGRect) {
super.draw(rect);
UIGraphicsBeginImageContext(self.frame.size)
UIImage(named: "Location.Info-background")?.draw(in: self.bounds)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.backgroundColor = UIColor(patternImage: image!)
}
override func setSelected(_ selected: Bool, animated: Bool) {
//todo
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
//todo
return nil
}
}
My custom annotationpin class is as follows
import Foundation
import MapKit
import UIKit
class MapPin: MKPointAnnotation{
var name: String
var street: String
var type: String
var postCode: String
init(name: String, street: String, type: String, postCode: String){
self.name = name
self.street = street
self.type = type
self.postCode = postCode
}
}
and I'm trying to use this all as follows in my view controller class
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
CLGeocoder().reverseGeocodeLocation(manager.location!) { (placemarks, error) in
if (error != nil){
return
}
if placemarks?.count != nil{
let pm = (placemarks?[0])! as CLPlacemark
self.displayLocationInfo(placemark: pm)
}
}
let spanX = 0.00725
let spanY = 0.00725
locationManager.stopUpdatingLocation()
let location = locations.last! as CLLocation
let center = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: spanX, longitudeDelta: spanY))
self.MapRSR.setRegion(region, animated: true)
self.MapRSR.delegate = self
let mapPin = MapPin(name: "", street: "", type: "", postCode: "")
mapPin.coordinate = center
mapPin.title = "test"
self.MapRSR.addAnnotation(mapPin)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let pin = mapView.dequeueReusableAnnotationView(withIdentifier: "LocationInfo") ?? AddressView(annotation: annotation, reuseIdentifier: "LocationInfo")
pin.canShowCallout = true
return pin
}
It just won't show my xib view. Does anyone know what I'm doing wrong or how I can achieve the effect I want which is something like this.
In didSelectAnnotationView load xib from bundle and add subview to the annotation view. here CustomXibCallout is xib file and CustomCalloutView is MKAnnotationView.
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
if view.annotation!.isKindOfClass(MKUserLocation){
return
}
//Custom xib
let customView = (NSBundle.mainBundle().loadNibNamed("CustomXibCallout", owner: self, options: nil))[0] as! CustomCalloutView;
let calloutViewFrame = customView.frame;
customView.frame = CGRect(x: -calloutViewFrame.size.width/2.23, y: -calloutViewFrame.size.height-7, width: 315, height: 200)
view.addSubview(customView)
}
in didDeselectAnnotationView remove the added view
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView)
{
for childView:AnyObject in view.subviews{
childView.removeFromSuperview();
}
}
Example for CustomCallout
This is what my project currently looks like. My questions is, how do I change the blue ball (current location) to a custom image or icon?
I am sure you know that a user is used to seeing that blue-dot as the current user's location. You shouldn't change it unless you have a good reason.
Here is how to change it:
Set the delegate for the mapView, and then override the following function... something like this:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
let pin = mapView.view(for: annotation) as? MKPinAnnotationView ?? MKPinAnnotationView(annotation: annotation, reuseIdentifier: nil)
pin.pinTintColor = UIColor.purple
return pin
} else {
// handle other annotations
}
return nil
}
and to have an image displayed instead:
Just replace the code inside if statement with the following code:
let pin = mapView.view(for: annotation) as? MKPinAnnotationView ?? MKPinAnnotationView(annotation: annotation, reuseIdentifier: nil)
pin.image = UIImage(named: "user_location_pin")
return pin
I think this code sample should give you enough information to help you figure out what to do. (Note that mapView is created in a storyboard...)
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
let loc = CLLocationManager()
var angle = 0
var timer: NSTimer!
var userPinView: MKAnnotationView!
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
loc.requestWhenInUseAuthorization()
timer = NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: #selector(rotateMe), userInfo: nil, repeats: true)
}
func rotateMe() {
angle = angle + 10
userPinView?.transform = CGAffineTransformMakeRotation( CGFloat( (Double(angle) / 360.0) * M_PI ) )
}
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
let pin = mapView.viewForAnnotation(annotation) ?? MKAnnotationView(annotation: annotation, reuseIdentifier: nil)
pin.image = UIImage(named: "userPinImage")
userPinView = pin
return pin
} else {
// handle other annotations
}
return nil
}
}
You can customise the view using MKMapDelegate's method:
optional func mapView(_ mapView: MKMapView,
viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?
Parameters
mapView - The map view that requested the annotation view.
annotation - The object representing the annotation that is about to be displayed. In addition to your custom annotations, this object
could be an MKUserLocation object representing the user’s current
location.
See full the documentation here
Also please see the following SO question for updating the view when user location changes:
Custom Annotation view for userlocation not moving the mapview
I have the following code that fetches the gps coordinates from the user and puts a red marker in the place where he currently is:
class ViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet var mapView: MKMapView!
var locationManager: CLLocationManager?
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager!.delegate = self
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
locationManager!.startUpdatingLocation()
} else {
locationManager!.requestWhenInUseAuthorization()
}
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .NotDetermined:
print("NotDetermined")
case .Restricted:
print("Restricted")
case .Denied:
print("Denied")
case .AuthorizedAlways:
print("AuthorizedAlways")
case .AuthorizedWhenInUse:
print("AuthorizedWhenInUse")
locationManager!.startUpdatingLocation()
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations.first!
let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate, 500, 500)
mapView.setRegion(coordinateRegion, animated: true)
locationManager?.stopUpdatingLocation()
let annotation = MKPointAnnotation()
annotation.coordinate = location.coordinate
longitude = location.coordinate.longitude
latitude = location.coordinate.latitude
mapView.addAnnotation(annotation)
locationManager = nil
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("Failed to initialize GPS: ", error.description)
}
}
That works great and as you can see, I'm using these lines to get the user's lat and long to process it later on:
longitude = location.coordinate.longitude
latitude = location.coordinate.latitude
Now I want to add another functionality - user see his current location on the map as the red pin and can leave it as it is OR - the new feature - he can drag the red guy somewhere else and get its new longitude and latitude.
I've been looking here and there at the draggable feature and I've decided to add this code to my existing one:
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is MKPointAnnotation {
let pinAnnotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "myPin")
pinAnnotationView.pinColor = .Purple
pinAnnotationView.draggable = true
pinAnnotationView.canShowCallout = true
pinAnnotationView.animatesDrop = true
return pinAnnotationView
}
return nil
}
But it didn't do the trick and the pin is still not draggable. How can I fix that and fetch (of for now - print to the console) the new location of the pin each time user grabs it and moves it around?
If you're seeing a red marker rather than the purple marker you specified in your code sample, that would suggest that you haven’t registered your annotation view’s reuse identifier and/or the mapView(_:viewFor:) isn't getting called.
Nowadays, to help avoid view controller bloat, we’d generally put the customization of the annotation view in its own subclass (in the spirit of the “single responsibility principle”):
class CustomAnnotationView: MKPinAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
pinTintColor = .purple
isDraggable = true
canShowCallout = true
animatesDrop = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then, in iOS 11 and later, you probably would remove the mapView(_:viewFor:) entirely and instead just register that class and you’re done:
override func viewDidLoad() {
super.viewDidLoad()
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
In iOS versions prior to 11 (or if you have some complicated logic where you might have multiple types of annotations that you want to add) you’d want to specify the delegate of the map view and implement mapView(_:viewFor:).
One can hook up the delegate in IB by control-dragging from the delegate outlet in the “Connections Inspector” in IB, or by doing it programmatically, e.g. in viewDidLoad:
mapView.delegate = self
I’d then add a constant to specify my preferred the reuse identifier for the custom annotation view:
class CustomAnnotationView: MKPinAnnotationView {
static let reuseIdentifier = Bundle.main.bundleIdentifier! + ".customAnnotationView"
// these two are unchanged from as shown above
override init(annotation: MKAnnotation?, reuseIdentifier: String?) { ... }
required init?(coder aDecoder: NSCoder) { ... }
}
And then, you’d implement mapView(_:viewFor:) which will attempt to dequeue an previous annotation view and update its annotation, if possible, or instantiate a new annotation view, if not:
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: CustomAnnotationView.reuseIdentifier) {
annotationView.annotation = annotation
return annotationView
}
return CustomAnnotationView(annotation: annotation, reuseIdentifier: CustomAnnotationView.reuseIdentifier)
}
}
See previous iteration of this answer for older Swift versions.
Tip: You can drag an annotation when it is selected, and it can be selected only when it shows a callout. It can show a callout, when it has a title. So unless you have a title, it won’t work.