Search code examples
swiftmkmapviewmkannotationmkannotationview

custom annotationView label overlap (causing MKMapView memory leak)


I am building an app using mkmapview. I am using custom annotation views to display avatars and corresponding labels. Everything works fine: Each avatar and the corresponding label are displayed properly. I can navigate in the map. But when I zoom out at the maximum, and there are let's say 4 labels being displayed in San Francisco, labels overlap once I get back to original scale. At max zoom out labels are on top of each other. This is normal. But once I get back to normal zoom:

   let span = MKCoordinateSpanMake(0.001, 0.001)

Each label looks like it has been stamped by others labels: PING map annotationview label overlaping

The label here above should say "sanchez" but it has melt with other labels texts.

Here is my code for the custom annotation view:

   func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
    if !(annotation is MKPointAnnotation) {
        return nil
    }
    var seleccion:Bool

    let reuseId = "test"
    var anView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
    if anView == nil {
        anView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
        anView.canShowCallout = true
    }
    else {
        anView.annotation = annotation
    }

    let cpa = annotation as! CustomPointAnnotation
    anView.image = cpa.image
    if cpa.toBeTriggered == true {
        anView.selected = true
    }
    var nameLbl: UILabel! = UILabel(frame: CGRectMake(-24, 40, 100, 30))
    nameLbl.text = cpa.nickName
    nameLbl.textColor = UIColor.blackColor()
    nameLbl.font = UIFont(name: "Atari Classic Extrasmooth", size: 10)
    nameLbl.textAlignment = NSTextAlignment.Center
    anView.addSubview(nameLbl)

    return anView
}

This is where it gets weird: If I set a background color for nameLbl, labels don't overlap/melt. Which makes me think of a bug from Apple...

EDIT

As @Anna mentionned, CustomPointAnnotation is a data-model class:

import UIKit
import MapKit

class CustomPointAnnotation: MKPointAnnotation {
    var image: UIImage!
    var toBeTriggered: Bool = false
    var selected: Bool = false
    var nickName: String!
}

Solution

  • There are two problems causing the label overlap:

    1. As mentioned in the comments, a UILabel is being added to the annotation view every time viewForAnnotation is called including when a view is being recycled (when mapView.dequeueReusableAnnotationViewWithIdentifier returns a view). A recycled view will already have a UILabel with text set on it. When you add another label on top of that, you get overlapping text.

      Instead of adding the label every time, only add it when actually creating an MKAnnotationView (when the dequeue returns nil). Then to set the label's text, get a reference to the label already on the view (setting a tag on it is a simple way).

      Another approach is to create a custom annotation view class (a subclass of MKAnnotationView) and implement prepareForReuse in there and clear the label's text. This custom view class would be a separate thing from the CustomPointAnnotation model class.

    2. Even after making the fix in problem 1, you will still get overlapping labels when zooming far out and there are multiple annotations close to each other. Their images and labels will overlap. This isn't a bug. That's just the way it is. To avoid this problem (if it's a problem for you), one solution is to implement annotation clustering where you merge or separate annotations depending on the zoom level. Explaining how to do that is out-of-scope of this question. You can search for annotation clustering libraries or implementation details if interested.

    To solve problem 1 in the quickest way, you could do the following:

    func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
        if !(annotation is CustomPointAnnotation) {
            //Check for CustomPointAnnotation (not MKPointAnnotation)
            //because the code below assumes CustomPointAnnotation.
            return nil
        }
        var seleccion:Bool
    
        let reuseId = "test"
        var anView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
        if anView == nil {
            anView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
            anView.canShowCallout = true
    
            //Create and add UILabel only when actually creating MKAnnotationView...
            var nameLbl: UILabel! = UILabel(frame: CGRectMake(-24, 40, 100, 30))
            nameLbl.tag = 42    //set tag on it so we can easily find it later
            nameLbl.textColor = UIColor.blackColor()
            nameLbl.font = UIFont(name: "Atari Classic Extrasmooth", size: 10)
            nameLbl.textAlignment = NSTextAlignment.Center
            anView.addSubview(nameLbl)
        }
        else {
            anView.annotation = annotation
        }
    
        let cpa = annotation as! CustomPointAnnotation
        anView.image = cpa.image
    
        //NOTE: Setting selected property directly on MKAnnotationView 
        //      is not recommended.
        //      See documentation for the property.
        //      Instead, call MKMapView.selectAnnotation method 
        //      in the didAddAnnotationViews delegate method.
        if cpa.toBeTriggered == true {
            anView.selected = true
        }
    
        //Get a reference to the UILabel already on the view
        //and set its text...
        if let nameLbl = anView.viewWithTag(42) as? UILabel {
            nameLbl.text = cpa.nickName
        }
    
        return anView
    }