Swift - Custom MKAnnotationView, set label title

TimWhiting picture TimWhiting · Jan 21, 2015 · Viewed 17k times · Source

I am trying to customise the MKAnnotationView for my mapView callout bubbles. I am fine with setting the annotation title when the annotation is created, and also customising the MKAnnotationView to add labels or images e.t.c (in the viewForAnnotation delegate), but how do I change the label created in the viewForAnnotation delegate, so that the title of it is different for each pin?

The other issue I have is that if I don't add a title or subtitle to the annotation when it is created in the viewDidLoad method, but I still try and create one by leaving self.map.addAnnotation(annotation), when I run the app and tap the pin no callout bubble is displayed.

In the end I would like to have totally customised callout bubbles, with individual labels on them for each pin. So what i really ned to know is how to access the viewForAnnotation delegate when the annotation is created to change properties of it for each pin.

override func viewDidLoad() {
        super.viewDidLoad()

        var countries: [String] = ["Germany","Germany","Poland","Denmark"]
        var practiceRoute: [CLLocationCoordinate2D] = [CLLocationCoordinate2DMake(50, 10),CLLocationCoordinate2DMake(52, 9),CLLocationCoordinate2DMake(53, 20),CLLocationCoordinate2DMake(56, 14)]

        for vari=0; i<practiceRoute.count; i++ {

            var annotation = MKPointAnnotation
            annotation.title = countries[i]
            annotation.coordinate = practiceRoute[i]
            self.map.addAnnotation(annotation)

        }

}

func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {

    if annotation is MKUserLocation {
        return nil
    }

    let reuseId = "pin"
    var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView

    if(pinView==nil){

        pinView=MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
        pinView!.canShowCallout = true

        let base = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
        base.backgroundColor = UIColor.lightGrayColor()

        let label1 = UILabel(frame: CGRect(x: 30, y: 10, width: 60, height: 15))
        label1.textColor = UIColor.blackColor()
        label1.text = "12 photos"
        base.addSubview(label1)
        pinView!.leftCalloutAccessoryView = base
        pinView!.pinColor = .Red

    }

    return pinView

}

Answer

Yuchen Zhong picture Yuchen Zhong · Jan 10, 2016

Make your custom annotation view

There is no public API allowing you to access the label in the pop up directly. What you need to do is make a subclass of MKPinAnnotationView and do whatever customization you want there. As an example,

class CustomAnnotationView : MKPinAnnotationView
{
    let selectedLabel:UILabel = UILabel.init(frame:CGRectMake(0, 0, 140, 38))

    override func setSelected(selected: Bool, animated: Bool)
    {
        super.setSelected(false, animated: animated)

        if(selected)
        {
            // Do customization, for example:
            selectedLabel.text = "Hello World!!"
            selectedLabel.textAlignment = .Center
            selectedLabel.font = UIFont.init(name: "HelveticaBold", size: 15)
            selectedLabel.backgroundColor = UIColor.lightGrayColor()
            selectedLabel.layer.borderColor = UIColor.darkGrayColor().CGColor
            selectedLabel.layer.borderWidth = 2
            selectedLabel.layer.cornerRadius = 5
            selectedLabel.layer.masksToBounds = true

            selectedLabel.center.x = 0.5 * self.frame.size.width;
            selectedLabel.center.y = -0.5 * selectedLabel.frame.height;
            self.addSubview(selectedLabel)
        }
        else
        {
            selectedLabel.removeFromSuperview()
        }
    }
}    

enter image description here

Other Notes

  1. Use this custom view in the map view:

    func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
        var anno = mapView.dequeueReusableAnnotationViewWithIdentifier("Anno")
        if anno == nil
        {
            anno = CustomAnnotationView.init(annotation: annotation, reuseIdentifier: "Anno")
        }
        return anno;
    }
    
  2. Since the title property of the annotation is not set, you will have to call the map view function selectAnnotation yourself. Add the following to the CustomAnnotationView class:

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        mapView?.selectAnnotation(self.annotation!, animated: true)
    }
    

If you want to have more than one marker on the map:

Usually just draw the annotation simply during initialization. In setSelected just return false (meaning "show all annotations all the time").

class DotAnnotationView : MKPinAnnotationView {

    let dot: UILabel = UILabel.init(frame:CGRect(x: 0, y: 0, width: 20, height: 20))

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        _setup()
    }

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        _setup()
    }

    override func prepareForReuse() {
        dot.text = "you forgot to set the text value?"
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(false, animated: animated)
    }

    func _setup() {
        dot.textAlignment = .center
        .. etc
    }

}

You set the string (or other values - say color of the panel) for each annotation in mapView#viewFor. It's like populating a cell in a UITableView.

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {

    let textForThisItem = annotation.title!!
    // or, just use index#of to determine which row this is in your data array
    if annotation.isEqual(mkMap.userLocation) {
        // skip the user-position indicator
        return nil
    }

    var anno = mapView.dequeueReusableAnnotationView(withIdentifier: "anno")

    if anno == nil {
        anno = DotAnnotationView.init(annotation: annotation, reuseIdentifier: "anno")
    }

    (anno as! DotAnnotationView).dot.text = textForThisItem

    return anno
}

Finally note that somewhat confusingly, if you very simply change the class of CustomAnnotationView from MKPinAnnotationView to MKAnnotationView, everything works the same but it replaces "all of the pin" rather than just the annotation.