Challenge saving and deleting MapPoints


#1

I have found a solution to the challenge.
I added the possibility to remove indivdual or all MapPoints.
The only bug is with multiple MapPoint in the same location, you can only remove the frontmost MapPoint.
I hope someone will look at the code and tell me if I could do things better.
A solution to the bug is also very welcome.

What I have done:
I copied the FileHelpers- .h and .m files from Homepwner app into this project.
Added #Import FileHelpers.h at the top of WhereAmIAppDelegate.h

Added this to MapPoint.h

@interface MapPoint : NSObject <MKAnnotation, NSCoding>

Added this code to MapPoint.m

-(void)encodeWithCoder:(NSCoder *)encoder {
    // for each instance variable archive it with its own name for key
    // split CLLocationCoordinate2D in latitude and longitude
    [encoder encodeDouble:coordinate.latitude forKey:@"coordinatelatitude"];
    [encoder encodeDouble:coordinate.longitude forKey:@"coordinatelongitude"];
    [encoder encodeObject:title forKey:@"title"];
    [encoder encodeObject:subtitle forKey:@"subtitle"];
}

-(id)initWithCoder:(NSCoder *)decoder {
    double latitude = [decoder decodeDoubleForKey:@"coordinatelatitude"];
    double longitude = [decoder decodeDoubleForKey:@"coordinatelongitude"];
    self = [super init];
    if (self) {
        coordinate = CLLocationCoordinate2DMake(latitude, longitude);
        [self setTitle:[decoder decodeObjectForKey:@"title"]];
        [self setSubtitle:[decoder decodeObjectForKey:@"subtitle"]];
    }
    return self;
}

// ====  for debugging
-(NSString *)description {
    return [NSString stringWithFormat:@"MPtitle: %@", title];
}

Added two buttons to MainWindow.xib, one for “clearAll”, one for "clearSelected"
Added 2 instance variables and a button outlet to WhereAmIAppDelegate.h

NSMutableArray *locations;
MKAnnotationView *selectedMapPoint;
IBOutlet UIButton *clearSelectedButton;// connect with clearSelected button

Added 4 methods to WhereAmIAppDelegate.h

-(NSString *)annotationArchivePath;
-(BOOL) saveAnnotations;
-(IBAction)clearAllMapPoints:(id)sender;// connect with clearAll button
-(IBAction)clearSelectedMapPoint:(id)sender;// connect with clearSelected button

Added the following code to WhereAmIAppDelegate.m
In the method: - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
after the line:
[worldView setShowsUserLocation:YES];

[clearSelectedButton setHidden:YES];
locations = [[NSMutableArray alloc] init];
NSString *path = [self annotationArchivePath];
NSArray *temp = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
[worldView addAnnotations:temp];
[locations addObjectsFromArray:temp];
// debug, see if mappoints loaded
NSLog(@"AppDidFinishLaunching-: %@", [worldView annotations]); 

In the method: -(void) foundLocation:(CLLocation *) loc
after the lines:
// Create an instance of MapPoint with the current data
MapPoint *mp = [[MapPoint alloc] initWithCoordinate:coord title:[locationTitleField text]];

[locations addObject:mp]; // for archiving

Added these 8 methods to WhereAmIAppDelegate.m

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    NSLog(@"App did enter background");
    [self saveAnnotations];
    [worldView removeAnnotations:locations];
    [locations removeAllObjects];
}

-(void)applicationWillEnterForeground:(UIApplication *)application
{
    NSString *path = [self annotationArchivePath];
    NSArray *temp = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    if (temp) {
        [worldView addAnnotations:temp];
        [locations addObjectsFromArray:temp];
    }
    // shows all MapPoints saved
    NSLog(@"AppWillEnterForeground-annotations: %@", [worldView annotations]);
}

- (IBAction)clearAllMapPoints:(id)sender 
{
    [worldView removeAnnotations:locations];
    [locations removeAllObjects];
    // shows mappoints left, should show only MKUserLocation
    NSLog(@"ClearMapPoints-annotations: %@", [worldView annotations]);
}

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
    NSLog(@"selected annotation view: %@", view);
    [clearSelectedButton setHidden:NO];
    selectedMapPoint = view;
}

- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
	// does not function well with multiple pins on same location
    // then the button stays hidden or is inactive, 
    // with multiple touches to select different MapPoint
	selectedMapPoint = nil;
    [clearSelectedButton setHidden:YES];
}

- (IBAction)clearSelectedMapPoint:(id)sender 
{
    if (selectedMapPoint != nil) { // when annotation is selected
        [locations removeObjectIdenticalTo:[selectedMapPoint annotation]];
        [worldView removeAnnotation:[selectedMapPoint annotation]];
        [clearSelectedButton setHidden:YES];
        selectedMapPoint = nil;
        // shows number of mappoints left
        NSLog(@"ClearSelectedMapPoint-annotations: %@", [worldView annotations]);
    }
}

-(BOOL) saveAnnotations
{
    // save locations NSMutableArray to disk
    return [NSKeyedArchiver archiveRootObject:locations toFile:[self annotationArchivePath]];
}

-(NSString *)annotationArchivePath
{
    // the returned path will be: Sandbox/Documents/annotations.data
    return  pathInDocumentDirectory(@"annotations.data");
}

Added following lines to -(void)dealloc:

[worldView release];
[activityIndicator release];
[locationTitleField release];
[clearSelectedButton release];
[locations release];

#2

Hi,

Good job.

I’ve knocked up a quick and dirty way to allow multiple selections (see below).

A more elegant solution though would be to cluster the annotations based on their proximity and the map scale - that is a lot more work though.

You will need to add to the WhereamiAppDelegate.h @interface to get the clickedButtonAtIndex to fire.

HTH
Gareth

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    NSString *selectedTitle = [actionSheet buttonTitleAtIndex:buttonIndex];
    
    NSUInteger idx = [[worldView annotations] indexOfObjectPassingTest:
                      ^ BOOL (MapPoint *mapPoint, NSUInteger idx, BOOL *stop)
                      {
                          return [[mapPoint title] isEqualToString:selectedTitle];
                      }];
    
    MapPoint *selectedAnnotation = [worldView.annotations objectAtIndex: idx];
    
    [worldView setDelegate:nil];  //hack to stop didSelectAnnotationView retriggerring again
    [worldView selectAnnotation:selectedAnnotation animated:YES];
    [worldView setDelegate:self];
    selectedMapPoint = [worldView viewForAnnotation:selectedAnnotation];
    [clearSelectedButton setHidden:NO];
}

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
    // Find out if there are multiple annotations for same coordinate
    NSMutableArray *duplicates = [[[NSMutableArray alloc] init] autorelease];
    for (MapPoint *mapPoint in [mapView annotations]) {
        if (! [mapPoint isKindOfClass:[MKUserLocation class]])  // ignore current location
        {
            if ([mapPoint coordinate].latitude == [[view annotation] coordinate ].latitude &&
                [mapPoint coordinate].longitude == [[view annotation] coordinate ].longitude) {
                [duplicates addObject];
            }
        }
    }
    
    if ([duplicates count] > 1)
    {
        [worldView deselectAnnotation:[view annotation] animated:YES];
        UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"Duplicates"
                                                                 delegate:self
                                                        cancelButtonTitle:nil
                                                   destructiveButtonTitle:nil
                                                        otherButtonTitles:nil];
        
        for (MapPoint *mapPoint in duplicates) {
            [actionSheet addButtonWithTitle:[mapPoint title]];
        }
        [actionSheet showInView:_window];
        [actionSheet release];
    }
    else
    {
        [clearSelectedButton setHidden:NO];
        selectedMapPoint = view;
    }    
}

#3

Hi Gareth

Thanks for your reply.
I have tried to use your methods in the WhereAmI app, but when I build the app I get the following error:
incompatible block pointer types initializing ‘signed char (^)(struct MapPoint *, NSUInteger, BOOL *)’, expected ‘BOOL (^)(struct objc_object *, NSUInteger, BOOL *)’

I do not quite understand the problem here.
Maybe you can help me with this.

Sicco.


#4

Hi,

Sorry about that - I’ve migrated to iOS 5 so there may be some differences

try this:

[code]// NSUInteger idx = [[worldView annotations] indexOfObjectPassingTest:
// ^ BOOL (MapPoint *mapPoint, NSUInteger idx, BOOL *stop)
// {
// return [[mapPoint title] isEqualToString:selectedTitle];
// }];
//
// MapPoint *selectedAnnotation = [worldView.annotations objectAtIndex: idx];

MapPoint *selectedAnnotation;
for (MapPoint *annotation in [worldView annotations]) {
    if ([[annotation title] isEqualToString:selectedTitle]) {
        selectedAnnotation = annotation;
        break;
    }
}

[/code]

Gareth


#5

Hi,

Works very good.
Interesting, using this actionsheet, did not know about it yet.
I will certainly start experimenting with your code.
Thanks a lot!

Sicco.