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
Related
I am very confused why this is displaying the default image instead of a round blue circle over New York. Any insight about this as well as when the default image is used will be greatly appreciated.
import UIKit
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
setupMapview()
}
func setupMapview(){
let mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.setCenter(CLLocationCoordinate2D(latitude: 40.74699, longitude: -73.98742), zoomLevel: 9, animated: false)
view.addSubview(mapView)
let annotation = MGLPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: 40.77014, longitude: -73.97480)
mapView.addAnnotation(annotation)
mapView.delegate = self
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
print("CORDINATE")
print(annotation.coordinate)
if annotation is MGLPointAnnotation {
print("SET\n\n\n")
let av = RoundedAnnotationView(annotation: annotation, reuseIdentifier: "ResuseIdentifier")
av.configure()
return av
}
return nil
}
}
class RoundedAnnotationView: MGLAnnotationView{
func configure(){
backgroundColor = .blue
layer.cornerRadius = 24
clipsToBounds = true
}
}
Output:
iPhone_Screen
print_statements
The standard default annotation is being shown in NY because that is exactly what you are adding to the map in setupMapview. If you want the map to display the user's location, you have to tell it to do so:
mapView.addAnnotation(annotation)
mapView.showsUserLocation = true // This needs to be set explicitly.
mapView.delegate = self
As usual, when you want to have access to the user's location you have to ask permission by inserting the correct flag in the info.plist:
Privacy - Location When In Use Usage Description
along with some kind of explanatory string:
"We'd like to track you with our satellite."
If you are running your app on the simulator you can create a custom location:
Simulator -> Features -> Location -> Custom Location...
Annotations should be added after the map has completely loaded. I have a more detailed step by step solution: https://github.com/mapbox/mapbox-gl-native/issues/16492
I want to change the pin color from Red to Purple once the switch has been turned on. So far I have tried:
#IBAction func SwitchChanged(_ sender: Any){
if LegacySwitch.isOn == true {
annotation.pinTintColor = .purple
} else {
annotation.pinTintColor = .red
}
}
My switch is connected with:
#IBOutlet weak var LegacySwitch: UISwitch!
I created my pin in my ViewDidLoad. The coordinates of the pin come from another ViewController.
//Map Stuff
let Coordinates = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
annotation.coordinate = Coordinates
LocationMap.addAnnotation(annotation)
let center = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
self.LocationMap.setRegion(region, animated: true)
When ever I run the app, the pin continues to be red. The Action is encountered as I used a breakpoint to tell me it ran.
EDIT
I forgot to mention, I created the annotation variable above the ViewDidLoad.
var annotation = MyPointAnnotation()
I also have a MKPointAnnotation Class
class MyPointAnnotation: MKPointAnnotation {
var pinTintColor: UIColor?
}
Things that did not work:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")
if LegacySwitch.isOn {
annotationView.pinTintColor = .purple
} else {
annotationView.pinTintColor = .red
}
return annotationView
}
Distinguish between:
An annotation: a lightweight bundle of characteristics
An annotation view: what you see, supplied on the basis of the annotation through a call to the map view delegate's mapView(_:viewFor:).
You are changing the former but not the latter. All the action in that regard happens in mapView(_:viewFor:), but you have not shown that — nor is there any particular reason why it would be called just because you change a property of an annotation sitting off in an instance variable somewhere. You need to replace the annotation in the map, so as to get the annotation view to be regenerated.
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.
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.
I am trying to change pin image on the MKMapView in Swift, but unfortunately it don't work. Any Idea what I am doing wrong ? I saw some examples here, but did not worked.
import UIKit
import MapKit
class AlarmMapViewController: UIViewController {
#IBOutlet weak var map: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
showAlarms()
map.showsUserLocation = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func showAlarms(){
map.region.center.latitude = 49
map.region.center.longitude = 12
map.region.span.latitudeDelta = 1
map.region.span.longitudeDelta = 1
for alarm in Alarms.sharedInstance.alarms {
let location = CLLocationCoordinate2D(
latitude: Double(alarm.latitude),
longitude: Double(alarm.longtitude)
)
let annotation = MKPointAnnotation()
annotation.setCoordinate(location)
annotation.title = alarm.name
annotation.subtitle = alarm.description
mapView(map, viewForAnnotation: annotation).annotation = annotation
map.addAnnotation(annotation)
}
}
#IBAction func zoomIn(sender: AnyObject) {
}
#IBAction func changeMapType(sender: AnyObject) {
}
func mapView(mapView: MKMapView!,
viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is MKUserLocation {
//return nil so map view draws "blue dot" for standard user location
return nil
}
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
pinView!.canShowCallout = true
pinView!.animatesDrop = true
pinView!.image = UIImage(named:"GreenDot")!
}
else {
pinView!.annotation = annotation
}
return pinView
}
}
GreenDot picture is available and used on other places.
Don't forget to set:
map.delegate = self
And make sure your UIViewController implements the MKMapViewDelegate protocol.
If you forget to do this, your implementation of mapView:viewForAnnotation: won't be invoked for your map.
Besides, it looks like pinView!.animatesDrop = true breaks custom images. You'd have to set it to false, or use MKAnnotationView (which doesn't have an animatesDrop property).
See this related question if you want to implement a custom drop animation.
MKPinAnnotationView always use a pin image, can't be everride. You must use MKAnnotationView instead.
Be aware because property animatesDrop it's not a valid MKAnnotationView's property, Rem the line.