Generally you’d add, for example, a rightCalloutAccessoryView
and then implement calloutAccessoryControlTapped
, like shown in how do I make a pin annotation callout?
But you say:
I need to make the whole callout clickable
MapKit doesn’t have a delegate method to capture taps on the callout, itself, only on the accessory views. But you can add your own delegate to do this for you.
protocol CustomAnnotationViewDelegate: class {
func didTapCallout(for annotation: MKAnnotation)
}
class CustomAnnotationView: MKPinAnnotationView {
static let preferredReuseIdentifier = Bundle.main.bundleIdentifier! + ".customAnnotationView"
weak var delegate: CustomAnnotationViewDelegate?
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
canShowCallout = true
let tap = UITapGestureRecognizer(target: self, action: #selector(didTapAnnotationView(_:)))
self.addGestureRecognizer(tap)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func didTapAnnotationView(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: self)
// ignore taps on the annotation view, itself
if bounds.contains(location) { return }
// if we got here, we must have tapped on the callout
delegate?.didTapCallout(for: annotation!)
}
}
Then in iOS 11 and later, you can register this reuseIdentifier:
override func viewDidLoad() {
super.viewDidLoad()
mapView.register(CustomAnnotationView.self,
forAnnotationViewWithReuseIdentifier: CustomAnnotationView.preferredReuseIdentifier)
}
And your viewFor
could specify the delegate:
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier, for: annotation) as! CustomAnnotationView
annotationView.delegate = self
return annotationView
}
}
Or, if you need to support iOS versions prior to 11, you wouldn’t register the reuse identifier but would have to manually instantiate the CustomAnnotationView
yourself if it is not successfully dequeued:
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: CustomAnnotationView.preferredReuseIdentifier) as? CustomAnnotationView
if annotationView == nil {
annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: CustomAnnotationView.preferredReuseIdentifier)
annotationView?.delegate = self
} else {
annotationView?.annotation = annotation
}
return annotationView
}
}
Either way, you can now have your view controller conform to the new CustomAnnotationViewDelegate
:
extension ViewController: CustomAnnotationViewDelegate {
func didTapCallout(for annotation: MKAnnotation) {
print("tapped callout for \(annotation)")
}
}
But note that in the above, I’m adding the tap gesture recognizer in the CustomAnnotationView
init
method, to ensure that the tap gesture is created once and only once, when the annotation view is first created.