Help us understand the problem. What is going on with this article?

MapKit Custom callout for annotation with touch response

More than 1 year has passed since last update.

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
        self.addGestureRecognizer(tap)
    }

    @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 {
                    break
                }
            }
        }
        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{
        $0.removeFromSuperview()
    }

    let callout = CustomCalloutView()


    view.addSubview(callout)
    view.addConstraints([
        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
    }).first

    callout?.removeFromSuperview()
    callout = nil
    view.constraints.forEach{
        view.removeConstraint($0)
    }
}
cayozin
iOS/Android development (Swift/C++/Java/Kotlin/XML/Javascript), backend (php/mySQL) and Machine Learning (Python/Scala)
ayudante
いつもユーザー中心で技術者とコンサルタントがとことん考え抜く それがアユダンテです
https://ayudante.jp/
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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした