Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
OrganizationEventAdvent CalendarQiitadon (β)
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.


MapKit Custom callout for annotation with touch response

When the user clicks on a map annotation, a callout window is expected to appear. Here, we demonstrate how to add a custom callout window to the MKMapView when this event occurs, and how to ensure the callout responds to future touch events.

First, we must create the CustomCalloutView, a container which can hold any content. Regardless of the contents, we must set translatesAutoresizingMaskIntoConstraints to false to ensure AutoLayout constraints can be used. We also add a tap gesture recognizer so that this callout can respond to touch events.

class CustomCalloutView: UIView {
    convenience init() {
        self.translatesAutoresizingMaskIntoConstraints = false
        let tap = UITapGestureRecognizer(target: self, action: #selector(onTap))
        tap.numberOfTapsRequired = 1

    @objc func onTap() {}

We must also subclass MKAnnotationView and override touch event handlers to ensure that its subviews are considered during user touch events.

class CustomAnnotationViewClass : MKAnnotationView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let hitView = super.hitTest(point, with: event)
        if (hitView != nil)
            self.superview?.bringSubview(toFront: self)
        return hitView

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let rect = self.bounds
        var isInside: Bool = rect.contains(point)
        if(!isInside) {
            for view in self.subviews {
                isInside = view.frame.contains(point)
                if isInside {
        return isInside

When the MKMapView delegate is required to provide a view for an annotation, which is added to the map and should display our CustomCalloutView, we should ensure the CustomAnnotationViewClass is returned so that its touch behaviour can be exploited. Note that we disable the default callout here.

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    var view = mapView.dequeueReusableAnnotationView(withIdentifier: "identifier")
    if(nil == view) {
        view = CustomAnnotationViewClass(annotation: annotation, reuseIdentifier: "identifier")

    view?.annotation = annotation
    view?.image = UIImage(named: "someImage")
    view?.canShowCallout = false
    return view

When the user touches this annotation, we must must add the CustomCalloutView to the MKAnnotationView (which is in fact our CustomAnnotationViewClass from earlier) and constrain it to its parent. For visual effect, we can also animate its appearance (e.g. fade-in and grow to full-size).

func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {

    view.subviews.filter { $0 is CustomCalloutView }.forEach{

    let callout = CustomCalloutView()

        NSLayoutConstraint(item: callout,
                           attribute: .centerX,
                           relatedBy: .equal,
                           toItem: view,
                           attribute: .centerX,
                           multiplier: 1.0,
                           constant: 0
        NSLayoutConstraint(item: callout,
                           attribute: .bottom,
                           relatedBy: .equal,
                           toItem: view,
                           attribute: .top,
                           multiplier: 1.0,
                           constant: 0.0

    callout.alpha = 0.0
    callout.contentScaleFactor = 0.0
    UIView.animate(withDuration: 0.5) {
        callout.alpha = 1.0
        callout.contentScaleFactor = 1.0

Finally, we must not forget to remove the custom callout when the annotation is deselected

func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
    var callout = view.subviews.filter({ (subview) -> Bool in
        subview is CustomCalloutView

    callout = nil
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Help us understand the problem. What are the problem?