2

I'm doing the willChangeValueForKey and didChangeValueForKey calls but even with them the mapView delegate never runs through the mapView: viewForAnnotation: to update the thing. How can I force the MKAnnotation to be updated?

The annotationView.image isn't being updated when the image changes, nor is the annotationView.image. As far as I can tell by my NSLog line the mapView: viewForAnnotation: isn't being called again when the MKAnnotation is updated.

.h

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface MapAnnotation : NSObject <MKAnnotation>

@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
@property (nonatomic, retain) NSString *currentTitle;
@property (nonatomic, retain) NSString *currentSubTitle;
@property (nonatomic, retain) NSString *category;
@property (nonatomic, retain) NSString *pin;

- (NSString*) title;
- (NSString*) subtitle;
- (id) initWithCoordinate:(CLLocationCoordinate2D) c;
- (CLLocationCoordinate2D) getCoordinate;
- (void) addPlace:(MapAnnotation*) place;
- (int) placesCount;
- (void) cleanPlaces;
- (UIImage *) getPin;
@end

.m

#import "MapAnnotation.h"

@interface MapAnnotation()
@property (strong) NSMutableArray *places;
@end

@implementation MapAnnotation

@synthesize coordinate      = _coordinate;
@synthesize currentTitle    = _currentTitle;
@synthesize currentSubTitle = _currentSubTitle;
@synthesize places          = _places;
@synthesize category        = _category;
@synthesize pin             = _pin;


- (NSString*) subtitle {
    if ([self placesCount] == 1) {
        return self.currentSubTitle;
    }
    else{
        return @"";
    }
}


- (NSString*) title {
    if ([self placesCount] == 1) {
        return self.currentTitle;
    }
    else{
        return [NSString stringWithFormat:@"%d places", [self placesCount]];
    }
}


- (void)addPlace:(MapAnnotation *)place {
    [self willChangeValueForKey:@"title"];
    [self willChangeValueForKey:@"subtitle"];
    [self.places addObject:place];
    [self didChangeValueForKey:@"title"];
    [self didChangeValueForKey:@"subtitle"];
}


- (CLLocationCoordinate2D)getCoordinate {
    return self.coordinate;
}


- (void)cleanPlaces {
    [self willChangeValueForKey:@"title"];
    [self willChangeValueForKey:@"subtitle"];
    [self.places removeAllObjects];
    [self.places addObject:self];
    [self didChangeValueForKey:@"title"];
    [self didChangeValueForKey:@"subtitle"];
}


- (id)initWithCoordinate:(CLLocationCoordinate2D) c {
    self.coordinate=c;
    self.places=[[NSMutableArray alloc] initWithCapacity:0];
    return self;
}


- (int)placesCount {
    return [self.places count];
}


- (UIImage *)getPin {
    // begin a graphics context of sufficient size
    CGSize size = CGSizeMake(26, 26);
    UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);

    // get the context for CoreGraphics
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGRect imageRect = CGRectMake(0, 0, size.width, size.height);

    // Draw Dot
    CGRect circleRect = CGRectInset(imageRect, 4, 4);
    [[UIColor blackColor] setStroke];
    [[UIColor yellowColor] setFill];
    CGContextFillEllipseInRect(ctx, circleRect);
    CGContextStrokeEllipseInRect(ctx, circleRect);

    // Dot Content
    [[UIColor blackColor] setStroke];
    [[UIColor blackColor] setFill];
    CGAffineTransform transform = CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 0.0);
    CGContextSetTextMatrix(ctx, transform);
    CGContextSetLineWidth(ctx, 2.0);
    CGContextSetCharacterSpacing(ctx, 1.7);
    CGContextSetTextDrawingMode(ctx, kCGTextFill);
    UIFont *font = [UIFont fontWithName:@"Arial" size:11.0];
    if ([self placesCount] != 1) {
        NSString *label = [NSString stringWithFormat:@"%d", [self placesCount]];
        CGSize stringSize = [label sizeWithAttributes:@{NSFontAttributeName:font}];
        [label drawAtPoint:CGPointMake(size.width / 2 - stringSize.width / 2, size.height / 2 - stringSize.height / 2)
            withAttributes:@{NSFontAttributeName:font}];
    } else {
        NSString *label = [NSString stringWithFormat:@"%@", self.pin];
        CGSize stringSize = [label sizeWithAttributes:@{NSFontAttributeName:font}];
        [label drawAtPoint:CGPointMake(size.width / 2 - stringSize.width / 2, size.height / 2 - stringSize.height / 2)
            withAttributes:@{NSFontAttributeName:font}];
    }


    // make image out of bitmap context
    UIImage *retImage = UIGraphicsGetImageFromCurrentImageContext();

    // free the context
    UIGraphicsEndImageContext();

    return retImage;
}

@end

mapView: viewForAnnotation:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
    MKAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:@"MyAnnoation"];
    if(!annotationView) {
        annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"MyAnnoation"];
        annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
    }

    annotationView.enabled = YES;
    if ([(MapAnnotation*)annotation placesCount] == 1) {
        annotationView.canShowCallout = YES;
    }
    annotationView.image = [(MapAnnotation*)annotation getPin];
    NSLog(@"%@", [annotation title]);

    return annotationView;
}
Justin808
  • 20,859
  • 46
  • 160
  • 265
  • Only the annotation view's _callout_ automatically observes changes to the title and subtitle. The view itself (including the image) does not observe those properties and will not automatically update the view. Are you using a custom annotation view (as well as a custom annotation)? Is the view observing changes to title and subtitle? Show the viewForAnnotation method. Alternatively, you can manually update the view when you update the annotation. –  Nov 17 '13 at 16:52
  • @AnnaKarenina - No custom view at the moment. My issue is that I'm changing annotation icon, thats not updating. I'll add some more code. – Justin808 Nov 17 '13 at 19:38

1 Answers1

1

As mentioned in the comment, except for title and subtitle, changes to annotation properties don't automatically cause the view to get updated.

Changing the title and subtitle will automatically update the callout while it's displayed as long as the changes result in KVO notifications (eg. if done using the default setter with ann.title = xxx or by manually issuing KVO notifications as the code in the question is doing since you have custom getters).

To automatically update the image, you'd probably need to use a custom annotation view that observes changes to the underlying annotation's properties and refreshes itself (in init call addObserver, in observeValueForKeyPath update image, in dealloc call removeObserver). You could also probably do it using non-KVO methods such as custom notifications or delegates (but still using a custom annotation view).

With the existing code, a quick and manual way to update the annotation view without creating a custom annotation view and using KVO, etc. is to obtain the annotation's current view right after the annotation is updated and update the image property. This should work as long as the code in viewForAnnotation has the same logic for setting the image.

I assume somewhere in the app (sometime after the annotation is already added), it is updating the places in the annotation by calling addPlace. After that, you can try the following:

[someExistingAnnotation addPlace:anotherPlace];

//obtain the current view of someExistingAnnotation...
MKAnnotationView *av = [mapView viewForAnnotation:someExistingAnnotation];

//update its image property to force refresh...
av.image = [someExistingAnnotation getPin];
//also set canShowCallout (it might have been NO)


Unrelated but the current viewForAnnotation doesn't properly handle possible view re-use and/or annotation types other than your custom annotation (such as the user location blue dot which would cause the current code to crash). I suggest changing it to this:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
    if (! [annotation isKindOfClass:[MapAnnotation class]])
    {
        //annotation is NOT a "MapAnnotation", return nil for default view...
        return nil;
    }

    MKAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:@"MyAnnoation"];
    if(!annotationView) {
        annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"MyAnnoation"];
        annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
    }

    //update view's annotation to current in case it's being re-used
    annotationView.annotation = annotation;

    annotationView.enabled = YES;
    if ([(MapAnnotation*)annotation placesCount] == 1) {
        annotationView.canShowCallout = YES;
    }
    else {
        annotationView.canShowCallout = NO;
    }

    annotationView.image = [(MapAnnotation*)annotation getPin];
    NSLog(@"%@", [annotation title]);

    return annotationView;
}