viewForAnnotation confusion and customizing the pinColor iteratively

Kokanee picture Kokanee · Jul 7, 2014 · Viewed 7.6k times · Source

The goal is to customize the pin colors per some values stored in an structure array.

Per some help here I implemented the the following viewForAnnotation delegate method and that works great calling this delegate method iteratively in a loop based on the size of my structure data array. So it works if I want to set all the pins to one color, purple for example (which is the commented line in the code below).

The problem is when I put in a switch to set the color based on a value in my array it goes through this code but does not respect any of the case values to set it to an alternate color and everything goes to a red pin (seemingly the default). I've printed out the status and debugged to know it is getting inside the switch and setting pinColor's accordingly but they don't seem to stick.

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

       let theindex = mystructindex  // grab the index from a global to be used below

        if annotation is MKUserLocation {
            //return nil so map view draws "blue dot" for standard user location
            return nil
        }

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

        if pinView == nil {
            //println("Pinview was nil")
            pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
            pinView!.canShowCallout = true
            pinView!.animatesDrop = true

            // Preventive if to keep this from being called beyond my arrays index value as the delegate getting called beyond the for loop for some unknown reason

            if (theindex < MySupplierData.count) {

                // Set the pin color based on the status value in MySupplierData structure array
                switch MySupplierData[mystructindex].status  {

                case 0,1:
                    println("Case 0 or 1 - setting to Red")
                    pinView!.pinColor = .Red  // Needs help, show red pin
                case 2:
                    println("Case 2 - Setting to Green")
                    pinView!.pinColor = .Green  // Looking Good 
                case 3:
                    println("Case 3 - Setting to Purple")
                    pinView!.pinColor = .Purple  // Could use a follow-up
                default:
                    println("Case default - Should Never Happen")
                    break;

                }   // end switch
            } // end if

            // pinView!.pinColor = .Purple  // This works fine without the switch and respects any color I set it to.
        }
        else {
            pinView!.annotation = annotation
        }

        return pinView
}

Inside my for loop within the ViewController I call this as follows, but I don't do anything with the return.

        // previous to this I setup some Titles and Subtitle which work fine
        self.theMapView.addAnnotation(myAnnotation)
        // Call to my mapview   
        mapView(theMapView, viewForAnnotation: myAnnotation)

I don't do anything with the return Pinview - didn't think I needed to but all the pins get drawn red at this point when using the switch code. Fundamentally I must be missing something here.


7-8-14 Updates to address problems with revised code per Anna's great help/tutoring. TKS!

It almost works, All pins within the Map have the right colors but ones outside of the immediate display are sometimes wrong. Posting all the code involved here since it may help others as this seems to be a very common question on how to do custom work within Maps.

A custom class as suggested to hold other variable in a custom annotation - in this case the status value coming from my data structure, MySupplierData.

class CustomMapPinAnnotation : NSObject, MKAnnotation {
  var coordinate: CLLocationCoordinate2D
  var title: String
  var subtitle: String
  var status: Int

  init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, status: Int) {
    self.coordinate = coordinate
    self.title = title
    self.subtitle = subtitle
    self.status = status

  }
}

The revised mapView - now utilizing the new CustomMapPinAnnotation being passed to it:

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

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

        if pinView == nil {
            //println("Pinview was nil")
            pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
            pinView!.canShowCallout = true
            pinView!.animatesDrop = true

            // Code to catch my custom CustomMapPinAnnotation so we can check the status and set the color               
            if annotation.isKindOfClass(CustomMapPinAnnotation)
            {
                println("FOUND OUR CustomMapPinAnnotation CLASS IN mapView")
                println(" Custom Title = \(annotation.title)")
                println(" Custom status passed = \(annotation.status)")
                switch annotation.status {

                case 0,1:
                    println("Case 0 or 1 - Setting to Red")
                    pinView!.pinColor = .Red
                case 2:
                    println("Case 2 - Setting to Green")
                    pinView!.pinColor = .Green
                case 3:
                    println("Case 3 - Setting to Purple")
                    pinView!.pinColor = .Purple 
                default:
                    println("Case default - Should Never Happen")
                    break;
                }  // switch   
            }  // if     
        }
        else {
            pinView!.annotation = annotation
        }
        return pinView
} //func mapView

Within viewDidLoad the setup and For loop to setup the annotations

override func viewDidLoad() {
    super.viewDidLoad()

    // setup the region and Span 
    var theSpan:MKCoordinateSpan = MKCoordinateSpanMake(latDelta, longDelta)

    // Set the region to the the first element of the structure array.
    var theRegion:MKCoordinateRegion = MKCoordinateRegionMake(CLLocationCoordinate2DMake(MySupplierData[0].latitude, MySupplierData[0].longitude), theSpan)

    // This set the Map Type (Standard, Satellite, Hybrid)
    self.theMapView.mapType = MKMapType.Standard

    // Now loop through the structure data from 1 top the end of the structure to map the data

    var mytitle: String = ""
    var mysubtitle: String = ""
    var myCustomPinAnnotation: CustomMapPinAnnotation

    for mystructindex = 0; mystructindex < MySupplierData.count; ++mystructindex {           
        println("INSIDE SUPPLIER LOOP INDEX = \(mystructindex)" )

        switch MySupplierData[mystructindex].status {
        case 0:
            mytitle =  "(Red) " + MySupplierData[mystructindex].company
        case 1:
            mytitle = "(Red) " + MySupplierData[mystructindex].company
        case 2:
            mytitle = "(Geeen) " + MySupplierData[mystructindex].company
        case 3:
            mytitle = "(Purple) " + MySupplierData[mystructindex].company
        default:
            mytitle = "? " + MySupplierData[mystructindex].company

        }    
        mysubtitle = MySupplierData[mystructindex].subtitle

         // Create the Custom Annotations with my added status code   
        myCustomPinAnnotation = CustomMapPinAnnotation(
            coordinate: CLLocationCoordinate2DMake(MySupplierData[mystructindex].latitude,MySupplierData[mystructindex].longitude),
            title: mytitle,        // custom title
            subtitle: mysubtitle,  // custom subtitle
            status: MySupplierData[mystructindex].status)  // status that will drive pin color

        // put this annotation in the view.
        self.theMapView.addAnnotation(myCustomPinAnnotation)
    }  // For

    // This line brings up the display with the specific region in mind, otherwise it seems to default to a US Map.
    self.theMapView.setRegion(theRegion, animated: true)

}  // viewDidLoad

Debug output shows the For loop executes to completion as expected to create the myCustomPinAnnotation's before the custom viewForAnnotation in mapView gets executed on its own internally. As I move the map to areas outside the immediate view I do note the viewForAnnotation in mapView gets called as needed and I see my switch executing accordingly but the pin colors are not always correct. All the pins within the initial display map are correct every time so it's these outer region ones I am stuck on currently as to why they are off.

Answer

user467105 picture user467105 · Jul 7, 2014

First, the code should not be calling viewForAnnotation explicitly itself.
Remove the explicit call to viewForAnnotation after the addAnnotation line.

viewForAnnotation is a delegate method and the map view will call it automatically when it needs to display an annotation. If it's not getting called automatically, make sure the map view's delegate property is set (to self for example).


Second (and the real issue), is that the code assumes that the viewForAnnotation delegate method will get called only once and immediately after adding each annotation.

This is not the case and is not guaranteed. The map view will call viewForAnnotation whenever it needs to display the annotation and could be called multiple times for the same annotation or long after the annotation is actually added (eg. after user pans or zooms the map and the annotation comes into view).

See does MKAnnotationView buffer its input queue? for some additional details and relevant links to other answers including sample code.

Basically, you must store the properties that affect an annotation's view with the annotation object itself and retrieve these properties from the annotation parameter that is passed into viewForAnnotation.


What I suggest for your case is this:

I assume you are using the built-in annotation class MKPointAnnotation. Instead of using MKPointAnnotation which does not let you store your custom status property alongwith the annotation object itself, either:

  • Create a custom class that implements the MKAnnotation protocol but also with a status property. Set this property when creating the annotation and extract its value from the annotation parameter passed into viewForAnnotation and set the pinColor accordingly. See sample code in linked answer(s).

  • Make the objects in the MySupplierData themselves objects that implement the MKAnnotation protocol. So if the objects in MySupplierData are instances of some class named, say, Supplier, make the Supplier class conform to the MKAnnotation protocol and then you can add the MySupplierData objects themselves to the map view when calling addAnnotation.