1

I have an application which shows something around 100 Annotations (with custom pin-images, callout accessories and annotation-images) in a MapView. While building the annotations I store a link between annotation and building so I can assign the right building and open the right segue afterwards.

In iOS 6 they get built really fast, I also enabled animation while adding them, so one pin got dropped after the other, but with apple maps in iOS7 this isn't possible anymore (?). Now building those 100 annotations takes over 1 second on my iPhone 4S and that's too long. Is there anyway to improve the code?

- (void)viewDidLoad

...

//creating annotations
    annotationlink = [[NSMutableArray alloc] init];

    for (int i = 0; i < data.count; i++) {
        NSDictionary *dataItem = [data objectAtIndex:i];

        //storing annotation in array for link
        Annotation *buildingannotation = [[Annotation alloc] init];
        NSNumber *index = [NSNumber numberWithInteger:i];
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:index, indexKey, buildingannotation, annotationKey, nil];
        [annotationlink addObject:dict];

        buildingannotation.title = [dataItem objectForKey:@"Building"];
        buildingannotation.subtitle = [dataItem objectForKey:@"Info"];

        MKCoordinateRegion buildingcoordinates;
        buildingcoordinates.center.latitude = [[dataItem objectForKey:@"Latitude"] floatValue];
        buildingcoordinates.center.longitude = [[dataItem objectForKey:@"Longitude"] floatValue];
        buildingannotation.coordinate = buildingcoordinates.center;

        [self.mapView addAnnotation:buildingannotation];
    }


- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
    if ([annotation isKindOfClass:[MKUserLocation class]]){
        return nil;
    }

    MKAnnotationView *pinView = (MKAnnotationView *)
    [self.mapView dequeueReusableAnnotationViewWithIdentifier:pinIdentifier];

    MKAnnotationView *customAnnotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:pinIdentifier];
    customAnnotationView.canShowCallout = YES;

    //right button to detail view
    UIButton* disclosureButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
    customAnnotationView.rightCalloutAccessoryView = disclosureButton;

    //left button for image
    NSInteger *buildingindex = [self getIndex:annotation];
    NSDictionary *dataItem = [data objectAtIndex:buildingindex];
    NSString* filename = [dataItem objectForKey:@"Thumb"];
    filename = [filename stringByAppendingString:@"@2x.jpg"];

    NSString* resourceimagePath = [resourcePath stringByAppendingPathComponent:filename];
    Image = [UIImage imageWithContentsOfFile:resourceimagePath];

    UIImageView *AnnotationThumb = [[UIImageView alloc] initWithImage:Image];
    AnnotationThumb.frame = CGRectMake(0, 0, 31, 31);
    customAnnotationView.leftCalloutAccessoryView = AnnotationThumb;

    //annotation image
    customAnnotationView.image = [UIImage imageNamed:@"Annotation_white.png"];

    return customAnnotationView;
    return pinView;
}

the following function gets the index of the current annotation using nspredicate to filter the array with the dictionaries. the advantage of this is the fact, that i can also use it when calloutAccessoryControlTapped:

-(NSInteger*) getIndex:(Annotation*)searchannotation
{
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == %@", annotationKey, searchannotation];
    NSArray *filteredarray = [annotationlink filteredArrayUsingPredicate:predicate];
    NSDictionary *building = [filteredarray objectAtIndex:0];
    NSInteger *buildingIndex = [[building objectForKey:indexKey] integerValue];
    return buildingIndex;
}

With an iPhone 4S the last pin is built 1.14 seconds after the view gets loaded.

if i search the annotation link array manually instead of using nspredicate function like this:

//left button for image
    int buildingIndex;
    for (int i = 0; i < annotationlink.count; i++) {
        NSDictionary *annotationDict = [annotationlink objectAtIndex:i];
        if ([[annotationDict objectForKey:annotationKey] isEqual:annotation]) {
            buildingIndex= [[annotationDict objectForKey:indexKey] integerValue];
            i = annotationlink.count;
        }
    }

    NSDictionary *dataItem = [data objectAtIndex:buildingIndex];

the log says that the last pin is built 1.89 seconds after the viewDidLoad.

if i create the annotations in viewDidApper instead of viewDidLoad the View is shown off course immediately but the background takes some time to load so until the pins are dropped everything is gray which is also not very nice...

Rich
  • 8,108
  • 5
  • 46
  • 59
user2014551
  • 325
  • 5
  • 17
  • well, i have to mention that the delay comes from the code in the `(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id )annotation`-method. when i log it there are sometimes gaps of up to 0.02 seconds between each pin... – user2014551 Apr 18 '14 at 11:39
  • The viewForAnnotation delegate method will be called every time the map needs to show that annotation (can be called multiple times for the same annotation). The `annotationLink` array is _not necessary_. Instead, put the data or references to the data you need for each annotation in the Annotation class itself and set this data before adding the annotation. See http://stackoverflow.com/questions/7921106/optimizing-code-for-mkmapview-large-number-of-annotations/7922056#7922056 for the idea. –  Apr 18 '14 at 11:59
  • For doing a segue using the selected annotation's data (without using this "annotationLink" approach and using the annotation itself as the data source), see this answer for an example: http://stackoverflow.com/questions/14805954/mkannotationview-push-to-view-controller-when-detaildesclosure-button-is-clicked –  Apr 18 '14 at 12:01
  • There are a few other inefficiencies in the code (viewForAnnotation ignoring the dequeue result, calculating the image filename in viewForAnnotation, etc) but I'm not able to answer in detail at the moment. –  Apr 18 '14 at 12:02
  • I had this problem before and realized that when i ran it on the main thread, it worked faster. – vnchopra Jan 16 '19 at 08:06

2 Answers2

1

Thank you Anna for your suggestions! I implemented the improvements like this:

Annotation.h:

#import <MapKit/MKAnnotation.h> 

@interface Annotation : NSObject <MKAnnotation> {} 
@property(nonatomic, assign) CLLocationCoordinate2D coordinate; 
@property(nonatomic, copy) NSString *title; 
@property(nonatomic, copy) NSString *subtitle; 
@property NSInteger *ID; 
@end 

Annotation.m:

#import "Annotation.h" 

@implementation Annotation 
@synthesize coordinate, title, subtitle, ID; 
@end 

ViewDidAppear:

//creating annotations 
for (int i = 0; i < data.count; i++) { 
   NSDictionary *dataItem = [data objectAtIndex:i]; 
   Annotation *buildingannotation = [[Annotation alloc] init]; 
   buildingannotation.ID = i; 
   buildingannotation.title = [dataItem objectForKey:@"Building"]; 
   buildingannotation.subtitle = [dataItem objectForKey:@"Subtitle"]; 

   MKCoordinateRegion buildingcoordinates; 
   buildingcoordinates.center.latitude = [[dataItem objectForKey:@"Latitude"] floatValue];    
   buildingcoordinates.center.longitude = [[dataItem objectForKey:@"Longitude"] floatValue]; 
   buildingannotation.coordinate = buildingcoordinates.center; 

   [self.mapView addAnnotation:buildingannotation]; 
} 

viewForAnnotation:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
    if ([annotation isKindOfClass:[MKUserLocation class]]){
        return nil;
}

MKAnnotationView *pinView = (MKAnnotationView *)
[self.mapView dequeueReusableAnnotationViewWithIdentifier:pinIdentifier];

if (pinView == nil) {
    MKAnnotationView *customAnnotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:pinIdentifier];
    customAnnotationView.canShowCallout = YES;

    //right button to detail view
    UIButton* disclosureButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
    customAnnotationView.rightCalloutAccessoryView = disclosureButton;

    //left button for image
    Annotation *buildingAnnotation = (Annotation *)annotation;
    NSInteger *buildingindex = buildingAnnotation.ID;

    NSString *filePath = [thumbname objectAtIndex:buildingindex];
    UIImageView *AnnotationThumb = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:filePath]];
    AnnotationThumb.frame = CGRectMake(0, 0, 31, 31);
    customAnnotationView.leftCalloutAccessoryView = AnnotationThumb;

    //annotation image
    customAnnotationView.image = [UIImage imageNamed:@"Annotation_white.png"];

    return customAnnotationView;
} else {
    pinView.annotation = annotation;
}

return pinView;
}

Callout:

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
    Annotation *buildingAnnotation = (Annotation *)view.annotation;
    selectedbuilding = buildingAnnotation.ID;
    [self performSegueWithIdentifier:@"DetailViewController" sender:self];
}

Takes still some time for showing all Annotations. Is there any chance to further improve the code?

user2014551
  • 325
  • 5
  • 17
  • well i figured out a problem with the code above. the pins get built totally correct the first time with the right images. when they are reused they show the right title and when i tap on them, the proper segue is opened. but the AnnotationThumb alias leftCalloutAccessoryView shows a different image on the reused pins. What's wrong? – user2014551 Apr 20 '14 at 16:40
  • Since the leftCalloutAccessoryView is annotation-specific, you have to determine and set it's image whether the view is being reused or not (that means after the if-else just before the `return pinView;`). Right now, it's only set when a view is created for some annotation x. If some annotation y reuses that view, it still has the image for annotation x. –  Apr 20 '14 at 17:04
0

I updated the vievForAnnotation function regarding to Anna's reply and the PhotosByLocation Sample Application. It works now and I hope it's the correct way to implement the reuse...

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
    if ([annotation isKindOfClass:[MKUserLocation class]]){
        return nil;
    }

    MKAnnotationView *buildingAnnotationView = (MKAnnotationView *) [mapView dequeueReusableAnnotationViewWithIdentifier:pinIdentifier];

    if (buildingAnnotationView) {
        [buildingAnnotationView prepareForReuse];
    } else {
        buildingAnnotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:pinIdentifier];
        buildingAnnotationView.canShowCallout = YES;

        //right button to detail view
        UIButton* disclosureButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
        buildingAnnotationView.rightCalloutAccessoryView = disclosureButton;

        //annotation image
        buildingAnnotationView.image = [UIImage imageNamed:@"Annotation_white.png"];
    }

    //left button for image
    Annotation *buildingAnnotation = (Annotation *)annotation;
    NSInteger *buildingindex = buildingAnnotation.ID;

    NSString *filePath = [thumbname objectAtIndex:buildingindex];
    UIImageView *AnnotationThumb = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:filePath]];
    AnnotationThumb.frame = CGRectMake(0, 0, 31, 31);
    buildingAnnotationView.leftCalloutAccessoryView = AnnotationThumb;

    return buildingAnnotationView;
}
user2014551
  • 325
  • 5
  • 17
  • I think you're mis-using prepareForReuse. Apple documents prepareForReuse as non-functional, and intended to be called when the view is returned to the queue, NOT when it is fetched for re-use. I suppose if it's only got weak references it would be fine to release it with a nil prepareForReuse, but I think you need to override prepareForReuse to release the annotation-specific bits before it returns to the free queue. – Taryn Dec 08 '17 at 20:49