4

How To Completely Customise Your Map Annotations Callout Views

 3 years ago
source link: https://sweettutos.com/2016/03/16/how-to-completely-customise-your-map-annotations-callout-views/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How To Completely Customise Your Map Annotations Callout Views

Update December 2017: Fully updated for Xcode 9 and Swift 4.

In a previous tutorial, you learnt how to customise the map annotations’ callouts. The detailCalloutAccessoryView property was a nice add to the MKAnnotationView class in iOS 9.

However, such extension was limited by only providing a left, right and detail callout views. More than that, you always had to assign a title string to the pin annotation in order to make the callout show up for you. And that actually wasn’t always useful.

So why does a complete annotation callout customisation comes in handy in some circumstances?

Nice question, let’s say you don’t want to put a title in your annotation callout? Well, just leave the title string empty! But that will lead to something like this:

The title has left an annoying white space at the top of the callout view.

Also, in many cases, you may find yourself uncomfortable with the three system-provided accessories views (left, detail and right) and would want to reorganize the callout elements in a way to add more controls to the view or set your own layout. Fortunately, MapKit leaves a room for you to completely customize the map annotations callout views.

And this tutorial is here to guide you, step by step, on how to do so.

As always, let’s do this!

Begin by downloading the starter project to work with here.

Expand the project files in the Project navigator view, you will notice I prepared a custom xib file called “CustomCalloutView” that contains three labels and a UIImageView, along with a UIView class to manage its outlets.

This UIView will replace the standard annotation callout. You may also change it and apply the Auto Layout constraints the way you want it to look.

Ok, let’s start by placing some annotations in the map. The annotations will represent some starbucks in the Paris area.

Select ViewController.swift file from the Project navigator view to open it in the editor. Then place the following declarations at the top, right after the class declaration:

var coordinates: [[Double]]!
var names:[String]!
var addresses:[String]!
var phones:[String]!

Next, locate the viewDidLoad method and implement the code below inside:

coordinates = [[48.85672,2.35501],[48.85196,2.33944],[48.85376,2.33953]]// Latitude,Longitude
names = ["Coffee Shop · Rue de Rivoli","Cafe · Boulevard Saint-Germain","Coffee Shop · Rue Saint-André des Arts"]
addresses = ["46 Rue de Rivoli, 75004 Paris, France","91 Boulevard Saint-Germain, 75006 Paris, France","62 Rue Saint-André des Arts, 75006 Paris, France"]
phones = ["+33144789478","+33146345268","+33146340672"]
self.mapView.delegate = self
for i in 0...2
  let coordinate = coordinates[i]
  let point = StarbucksAnnotation(coordinate: CLLocationCoordinate2D(latitude: coordinate[0] , longitude: coordinate[1] ))
  point.image = UIImage(named: "starbucks-\(i+1).jpg")
  point.name = names[i]
  point.address = addresses[i]
  point.phone = phones[i]
  self.mapView.addAnnotation(point)
let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 48.856614, longitude: 2.3522219000000177), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
self.mapView.setRegion(region, animated: true)

A quick explanation is needed here:

//1: Here you just filled the arrays with some starbucks coordinates and informations, also you set the current class as the map view delegate since we will implement some MKMapView protocol methods here.

//2: Looping over the starbucks arrays, instantiating an annotation for each object and add it to the map view. You will implement the StarbucksAnnotation custom class right away.

//3: After placing all the annotations, you just centered the map around the Paris area with a convenient zoom level.

Also, don’t forget to change the class declaration like below, so that the class now conforms to the MKMapViewDelegate protocol:

class ViewController: UIViewController, MKMapViewDelegate {

Alright, time to implement the custom MKAnnotation class that will handle a reference for all the pin informations (image, phone, coordinates, etc).

Select “File\New\File” from the menu and choose the “Swift File” template. Call it “StarbucksAnnotation” and hit the “Create” button.

Erase the default content that comes with the “StarbucksAnnotation” class and implement the following code inside:

import MapKit
class StarbucksAnnotation: NSObject, MKAnnotation {
    var coordinate: CLLocationCoordinate2D
    var phone: String!
    var name: String!
    var address: String!
    var image: UIImage!
    init(coordinate: CLLocationCoordinate2D) {
        self.coordinate = coordinate

As you can see, the class should conform to the MKAnnotation protocol in order to provide any annotation-related data. In our case, the phone, coordinate, name, address, image are all relevant to the annotation and will all be attached to the starbucks pin and fetched when necessary.

Now you can build and run the project successfully. All of the three pin annotations will be correctly placed on the map view.

Switch back to the “ViewController.swift” class and implement the following MKMapViewDelegate protocol method:

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {        
if annotation is MKUserLocation
      return nil
   var annotationView = self.mapView.dequeueReusableAnnotationView(withIdentifier: "Pin")
   if annotationView == nil{
       annotationView = AnnotationView(annotation: annotation, reuseIdentifier: "Pin")
       annotationView?.canShowCallout = false
   }else{
       annotationView?.annotation = annotation
   annotationView?.image = UIImage(named: "starbucks")
   return annotationView

The method above will be called each time an annotation is about to show in the map. Basically, it will ask for a reusable annotation from the map view. If no reusable object is available in the queue, it will instantiate a new MKAnnotationView object (instance of the “AnnotationView” class).

Note that you set the canShowCallout property to false, this is important since you gonna provide your own custom callout view for the annotation and hence you tell the system NOT to show the default provided callout.

Let’s create the “AnnotationView” class. Select “File\New\File” from the menu, choose “Swift File” template and name it “AnnotationView”. Remove its default content and replace it with the following:

import MapKit
class AnnotationView: 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

The code above is necessary to track the touch click on the custom callout views since you will no longer use the default annotation view callout, but instead a custom UIView 🙂

The project can now compile and run correctly, but the custom callouts views can not be shown yet.

In order to add the custom view and make it behave as the annotation callout, you will need to choose the right moment to do so. The correct moment is when the annotation is selected, at such event, you will add the custom view as a subview to the annotation view 🙂

The idea is clear now, fortunately there is a ready to go MKMapViewDelegate protocol method (mapView:didSelectAnnotationView:) that will let us know each time an annotation view is selected.

Switch to the “ViewController.swift” file and implement the following code before the class closing bracket:

func mapView(_ mapView: MKMapView,
        didSelect view: MKAnnotationView)
    if view.annotation is MKUserLocation
       // Don't proceed with custom callout
       return
    let starbucksAnnotation = view.annotation as! StarbucksAnnotation
    let views = Bundle.main.loadNibNamed("CustomCalloutView", owner: nil, options: nil)
    let calloutView = views?[0] as! CustomCalloutView
    calloutView.starbucksName.text = starbucksAnnotation.name
    calloutView.starbucksAddress.text = starbucksAnnotation.address
    calloutView.starbucksPhone.text = starbucksAnnotation.phone
    calloutView.starbucksImage.image = starbucksAnnotation.image
    let button = UIButton(frame: calloutView.starbucksPhone.frame)
    button.addTarget(self, action: #selector(ViewController.callPhoneNumber(sender:)), for: .touchUpInside)
    calloutView.addSubview(button)
    calloutView.center = CGPoint(x: view.bounds.size.width / 2, y: -calloutView.bounds.size.height*0.52)
    view.addSubview(calloutView)
    mapView.setCenter((view.annotation?.coordinate)!, animated: true)

Some explanation is needed here:

// 1: First, the method will check whether the selected annotation is a user location related annotation, in such case, it will return without doing anything.

// 2: Here, you cast the annotation to the “StarbucksAnnotation” class in order to fetch the related data for the starbucks objects. Then, you instantiated the “CustomCalloutView” xib file and set the name/address/phone/image data to show on the view. You also created a button with the same frame as the phone label in order to respond to the click and fire a method to launch the phone call.

// 3: Finally, you centered the callout view to align it right above the annotation view.

Implement the selector method that gets running when you click on the phone label:

    @objc func callPhoneNumber(sender: UIButton)
        let v = sender.superview as! CustomCalloutView
        if let url = URL(string: "telprompt://\(v.starbucksPhone.text!)"), UIApplication.shared.canOpenURL(url)
            UIApplication.shared.openURL(url)

The code from the method above will cast the selected view to retrieve the phone number from the CustomCalloutView property.

So far so good, you can now build and run the app. The callouts will show correctly. However, you will notice that the obvious behaviour of dismissing an annotation view callout when another annotation is selected is not occurring.

To make this happen, let’s implement one last protocol method in order to deselect an annotation view and always keep one callout view shown on the map.

Always inside the “ViewController.swift” file, Implement the following method code before the class closing brackets:

func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
        if view.isKind(of: AnnotationView.self)
            for subview in view.subviews
                subview.removeFromSuperview()

The code above will test whether the deselected annotation view is an instance of the “AnnotationView” class, in which case it will run through its subviews and remove them. Remember the “CustomCalloutView” object was added as a subview of the annotation view, and this makes it behave like any other subview in the annotation views hierarchy.

That’s it, run the app and enjoy your work 🙂

As usual, the final project source code is available for download here.

I would be interested to know how do you guys customize your map annotation views’ callouts? Let me know in the comments below.

If you enjoy this post, you can subscribe in the form below to get notified of all our upcoming tutorials, ebooks, apps and discounts.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK