Implementing NSCoding for MapPoint


#1

Hello,

Following through on the challenge to implement archiving in the Wherewasi app, I learn that CLLocationCoordinate2D must be wrapped in an NSValue in order to be encoded. As such I implemented the NSCoding protocol as follows:

- (void)encodeWithCoder:(NSCoder *)encoder
{
	// Archive title and subtitle under variable name
	[encoder encodeObject:title forKey:@"title"];
	[encoder encodeObject:subtitle forKey:@"subtitle"];
	
	// Coordinate must be wrapped in NSValue
	NSValue *coordinateValue = [NSValue valueWithBytes:&coordinate objCType:@encode(struct CLLocationCoordinate2D)]; 
	[encoder encodeObject:coordinateValue forKey:@"coordinate"];
}
	 
- (id)initWithCoder:(NSCoder *)decoder
{
	[super init];
	
	// Set title and subtitle...
	[self setTitle:[decoder decodeObjectForKey:@"title"]];
	[self setSubtitle:[decoder decodeObjectForKey:@"subtitle"]];
	
	// Extract coordinate from NSValue
	NSValue *coordinateValue = [decoder decodeObjectForKey:@"coordinate"];
	CLLocationCoordinate2D storedCoordinate;
	[coordinateValue getValue:&storedCoordinate];
	[self setCoordinate:storedCoordinate];

	return self;
}

My question concerns the use of NSValue here. In particular, the four lines in initWithCoder seem a little clumsy. Is there a neater idiom for this, or have I basically got it right?

Thanks in advance, and what a great book!

Regards,
Carlton


#2

Okay…

It turns out the NSValue approach complains about not being able to encode structs, which was why I went that way in the first place so I’m slightly confused… :slight_smile:

Back to the docs and I realise that CLLocation does conform to NSCoding so I’m wrapping the CLLocationCoordinate2D back in one of those.

  1. This is why I need to get my head round OCUnit! Cue Mutley impression :slight_smile:
  2. I’m still curious as to the usage of NSValue here.

Beyond that it turns out that some time can be spent working out why the setter for a synthesised readonly property is throwing an Unrecognized Selector exception… :wink:


#3

Hi,

I decided to break it down and encode latitude and longitude separately as doubles.

I guess there are pro’s and cons but it certainly makes it simpler.

Gareth


#4

I also ended up breaking Coordinate 2D into it’s components - coming from a different route;
I was importing latitude and longitude from a plist constructed from data on a web site (there should be some sort of Regex way of extracting thsese automatically… ) and putting these in a larger object which I would then interrogate to make MapPoints. But, it turns out (obvious with hindsight) that you can just bundle all the data into a single object and interrogate the title, subtitle and coordinate with methods in the object. As long as it implements MKAnnotation. Discovered this quite by accident as one of my attributes was called description and lo and behold, this was returned when I did NSLog(@"%@", theObjectsName). Very much an Aha! moment.

Very cool, this object oriented stuff.

On a side note, I love the compactness of the code in this book. Compared to some others which have really sprawly code, this is a joy.


#5

Thanks! We tried really hard to make sure we didn’t confuse the readers with too much extra fluff.

As a side note, the “compactness” of the code is really just a testament to the power of Cocoa Touch. When I review people’s code, I can immediately gauge their experience with Cocoa Touch (or Cocoa) given the length of their code. You would be surprised how many times I’ve seen code that replicates parts of Cocoa Touch because the programmer didn’t read the documentation and find that there was a class/method that already did what they wanted.


#6

Hi all,
Following up on the “compactness” thread, how do you recommend filtering the MKAnnotation objects before archiving them? Here is the code I used:

- (void) applicationWillTerminate:(UIApplication *)application
{
    NSString *savePath = [self annotationArrayPath];

    NSArray *annotations = [mapView annotations];
    NSMutableArray *mapPoints = [[NSMutableArray alloc] initWithArray:annotations];
    
    for (int i=[mapPoints count] - 1; i >= 0; i--) {
        if (!([[mapPoints objectAtIndex:i] conformsToProtocol:@protocol(NSCoding)]))
            [mapPoints removeObjectAtIndex:i];
    }
    
    if ([mapPoints count] > 0)
    {
        [NSKeyedArchiver archiveRootObject:mapPoints toFile];        
    }
}

The [mapView annotations] will contain a MKUserLocation (the blue dot) which does not follow the NSCoding protocol, and which needs to be removed from the array before archiving.

Is there a better way to get only the “MapPoint” annotations, or to filter an array without looping through each item?

Thanks for any info you can provide.

-Chris
p.s. your book is fantastic!


#7

Yeah, that’s pretty much what you have to do in that case. Although, I think this may be a cleaner approach:

NSArray *annotations = [mapView annotations];
NSMutableArray *mapPoints = [NSMutableArray array];
    
for (id obj in annotations) {
        if ([obj conformsToProtocol:@protocol(NSCoding)])
            [mapPoints addObject:obj];
}

#8

Could someone please post their implementation of the encoding/decoding and archiving/unarchiving of the MapPoint data? I don’t understand how this works. CLLocation conforms to the NSCoding protocol, yet you can’t directly archive CLLocationCoordinate2D because it’s a struct( I think). Would the doubles for latitude and longitude need to be split before archiving, and combined when archiving? I’ve tried everything I can think of, read the ‘Archives and Serializations Programming Guide’, scoured the Internet, consulted three books. I hate to be such a noob, but this is driving me crazy. I don’t feel like I can move forward until I understand this. I guess this is the ‘hard learning’ referred to in the book.


#9

Correct, you can’t archive the struct as is.

You have two choices:

  1. Just break the structure down into two floats and encode them as: @“coordinate.latitude”, @“coordinate.longitude”. (It doesn’t matter what you encode them as, as its just a string that you will use in initWithCoder:, but for readability, its nice to use something this distinguishable.)

  2. Instead of having MapPoint have CLLocationCoordinate2D instance variable, you could have the MapPoint carry a CLLocation instance variable. Since this conforms to NSCoding, you can just encode this object. Your implementation of the coordinate method (from the MKAnnotation protocol) would then just look like this:

- (CLLocationCoordinate2D)coordinate
{
     return [location coordinate];
}

#10

[quote=“JoeConway”]Correct, you can’t archive the struct as is.

You have two choices:

  1. Just break the structure down into two floats and encode them as: @“coordinate.latitude”, @“coordinate.longitude”. (It doesn’t matter what you encode them as, as its just a string that you will use in initWithCoder:, but for readability, its nice to use something this distinguishable.)

  2. Instead of having MapPoint have CLLocationCoordinate2D instance variable, you could have the MapPoint carry a CLLocation instance variable. Since this conforms to NSCoding, you can just encode this object. Your implementation of the coordinate method (from the MKAnnotation protocol) would then just look like this:

- (CLLocationCoordinate2D)coordinate
{
     return [location coordinate];
}
[/code][/quote]

I tried to implement option 2, but took out all the archiving code first, to make sure everything still worked. I added the CLLocation instance variable to MapPoint, and added the coordinate method in my MapPoint implementation. When I build/run my mapview shows correctly, but when I add a title, the screen shifts and then only displays blue. If I touch the annotation marker, everything looks correct, except for the map displaying. If I take out return [location coordinate] and replace with return coordinate in my coordinate method, it works so maybe I'm missing something else. 

Here is my code for Map Point, and WhereamiAppDelegate

[code]//
//  MapPoint.h
//  Whereami
//
//  Created by Peter Stanley on 7/25/10.
//  Copyright 2010 Peter Stanley. All rights reserved.
//

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

@interface MapPoint : NSObject <MKAnnotation,MKReverseGeocoderDelegate>
{
	MKPlacemark *placemark;
	NSString *title;
	CLLocation *location;
	CLLocationCoordinate2D coordinate;
}
@property (nonatomic, readonly) MKPlacemark *placemark;
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property (nonatomic, copy) NSString *title;

- (id)initWithCoordinate:(CLLocationCoordinate2D)c title:(NSString *)t;

@end

[code]//
// MapPoint.m
// Whereami
//
// Created by Peter Stanley on 7/25/10.
// Copyright 2010 Peter Stanley. All rights reserved.
//

#import “MapPoint.h”

@implementation MapPoint
@synthesize coordinate, title, placemark;

  • (id)initWithCoordinate:(CLLocationCoordinate2D)c title:(NSString *)t
    {
    [super init];
    coordinate = c;
    [self setTitle:t];
    return self;
    }

  • (CLLocationCoordinate2D)coordinate
    {
    return [location coordinate];
    }

  • (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFailWithError:(NSError *)error
    {
    [geocoder release];
    }

  • (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFindPlacemark:(MKPlacemark *)p
    {
    placemark = [p retain];
    [geocoder release];
    }

  • (NSString *)subtitle
    {
    return [NSString stringWithFormat:@"%@, %@",[placemark locality], [placemark administrativeArea]];
    }

  • (void)dealloc
    {
    [title release];
    [placemark release];
    [super dealloc];
    }
    @end
    [/code]

[code]//
// WhereamiAppDelegate.h
// Whereami
//
// Created by Peter Stanley on 7/24/10.
// Copyright Peter Stanley 2010. All rights reserved.
//

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

@interface WhereamiAppDelegate : NSObject
<UIApplicationDelegate, CLLocationManagerDelegate, MKMapViewDelegate>
{
UIWindow *window;
CLLocationManager *locationManager;
MKReverseGeocoder *reverseGeocoder;
IBOutlet MKMapView *mapView;
IBOutlet UIActivityIndicatorView *activityIndicator;
IBOutlet UITextField *locationTitleField;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

  • (void)findLocation;
  • (void)foundLocation;
  • (IBAction)setMapType:(id)sender;

@end[/code]

[code]//
// WhereamiAppDelegate.m
// Whereami
//
// Created by Peter Stanley on 7/24/10.
// Copyright Peter Stanley 2010. All rights reserved.
//

#import “WhereamiAppDelegate.h”
#import “MapPoint.h”

@implementation WhereamiAppDelegate

@synthesize window;

#pragma mark -
#pragma mark Application lifecycle

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Create location manager object
    locationManager = [[CLLocationManager alloc] init];

    // Make this instance of WhereamiAppDelegate the delegate
    // it will send its messages to our WhereamiAppDelegate
    [locationManager setDelegate:self];

    // We want all results from the location manager
    [locationManager setDistanceFilter:kCLDistanceFilterNone];

    // And we want it to be as accurate as possible
    // regardless of how much time/power it takes
    [locationManager setDesiredAccuracy:kCLLocationAccuracyBest];

    // Start reverse geocoder instance
    [reverseGeocoder start];

    // Start getting heading info
    //[locationManager startUpdatingHeading];

    // Tell our manager to start looking for its location immediately
    //[locationManager startUpdatingLocation];
    //[self findLocation];
    [mapView setShowsUserLocation:YES];

    [window makeKeyAndVisible];

    return YES;
    }

  • (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
    fromLocation:(CLLocation *)oldLocation
    {
    //How many seconds ago was this location created?
    NSTimeInterval t = [[newLocation timestamp] timeIntervalSinceNow];
    // CLLocationManagers will return the last found location of the
    // device first, you don’t want the data in this case.
    // If this location was made was more than 3 minutes ago, ignore it.
    if (t < -180) {
    // This is cached data, you don’t want it, keep looking
    return;
    }

    // Get and format date info
    NSDate *dateNow = [NSDate date];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setTimeStyle:NSDateFormatterShortStyle];
    [dateFormatter setDateStyle:NSDateFormatterShortStyle];
    NSString *dateString = [dateFormatter stringFromDate:dateNow];
    [dateFormatter release];

    // Build string for map annotation
    NSString *mapDate = [NSString stringWithFormat:@"%@:%@",[locationTitleField text],dateString];

    MapPoint *mp = [[MapPoint alloc]
    initWithCoordinate:[newLocation coordinate] title:mapDate];

    // Create Reverse geocoder object and build placemark string
    reverseGeocoder = [[MKReverseGeocoder alloc] initWithCoordinate:[newLocation coordinate]];
    [reverseGeocoder setDelegate:mp];
    [reverseGeocoder start];
    [mapView addAnnotation:mp];

    [mp release];
    [self foundLocation];
    }

//- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
//{
// NSLog(@“Heading: %@”, newHeading);
//}

  • (void)locationManager:(CLLocationManager *)manager
    didFailWithError:(NSError *)error
    {
    NSLog(@“Could not find location: %@”, error);
    }

  • (void)mapView:(MKMapView *)mv didAddAnnotationViews:(NSArray *)views
    {
    MKAnnotationView *annotationView = [views objectAtIndex:0];
    id mp = [annotationView annotation];
    MKCoordinateRegion region =
    MKCoordinateRegionMakeWithDistance([mp coordinate], 250, 250);
    [mv setRegion:region animated:YES];
    }

  • (BOOL)textFieldShouldReturn:(UITextField *)tf
    {
    [self findLocation];
    [tf resignFirstResponder];
    return YES;
    }

  • (void)findLocation
    {
    [locationManager startUpdatingLocation];
    [activityIndicator startAnimating];
    [locationTitleField setHidden:YES];
    }

  • (void)foundLocation
    {
    [locationTitleField setText:@""];
    [activityIndicator stopAnimating];
    [locationTitleField setHidden:NO];
    [locationManager stopUpdatingLocation];
    }

  • (IBAction)setMapType:(id)sender
    {
    switch (((UISegmentedControl *)sender).selectedSegmentIndex)
    {
    case 0:
    {
    mapView.mapType = MKMapTypeStandard;
    break;
    }
    case 1:
    {
    mapView.mapType = MKMapTypeSatellite;
    break;
    }
    default:
    {
    mapView.mapType = MKMapTypeHybrid;
    break;
    }
    }
    }

  • (void)applicationWillResignActive:(UIApplication )application {
    /

    Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
    */
    }

  • (void)applicationDidEnterBackground:(UIApplication )application {
    /

    Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    If your application supports background execution, called instead of applicationWillTerminate: when the user quits.
    */
    }

  • (void)applicationWillEnterForeground:(UIApplication )application {
    /

    Called as part of transition from the background to the inactive state: here you can undo many of the changes made on entering the background.
    */
    }

  • (void)applicationDidBecomeActive:(UIApplication )application {
    /

    Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    */
    }

  • (void)applicationWillTerminate:(UIApplication *)application {
    }

#pragma mark -
#pragma mark Memory management

  • (void)applicationDidReceiveMemoryWarning:(UIApplication )application {
    /

    Free up as much memory as possible by purging cached data objects that can be recreated (or reloaded from disk) later.
    */
    }

  • (void)dealloc {
    [locationManager setDelegate:nil];
    [mapView setDelegate:nil];
    [reverseGeocoder setDelegate:nil];
    [window release];
    [super dealloc];
    }

@end
[/code]


#11

The CLLocation object isn’t making its way to the MapPoint object. In locationManager:didUpdateToLocation:fromLocation:, you should set the location instance variable of the newly created MapPoint to the CLLocation instance you are given.


#12

That works! Thanks for your helping me work through this.


#13

i’m running into an interesting quandary, and I’m not at a point where I have my code handy which means I’m going to have to discuss this at a higher-than-code level. No matter, I think I can get the point across.

My code compiles no issues, no warnings, so that’s a positive.

When I launch the program, applicationDidFinishLaunchingWithOptions goes through the normal actions of initialising and looking for the datafile as needed. If it find a datafile, it implements the appropriate method to build an array of annotation objects, and then calls the addAnnotations(NSArray *)annotations method to plop them all down on the map.

As it finds no file at this point, it simply creates an empty array of annotation objects with:
annotationsArray = [[NSMutableArray alloc] init];

So far, so good. Up comes the map and I can add annotations to it just fine. Just after the call,
[mapView addAnnotation:mp];
I added [annotationArray addObject:mp]; which adds the annotation just created to the annotation array.

Still, so far, so good. As far as I can tell, the annotation is sitting comfortably, nestled safely in AnnotationArray, which is an instance variable so it should be accessible by all methods within the object.

When the time to write everything to disk comes (of course in applicationWillTerminate), I use [NSKeyedArchiver archiveObject:annotationArray toFile:annotationPath];

(obviously I’ve already set a value for annotationPath.)

so the archiver invokes encodeWithCoder inside MapPoint.m which runs fine until it hits this line:

[encoder encodeObject:placemark forKey:@“placemark”]; at which point it throws an exception: NSInvallidArgumentException - MKPlacemark encodeWithCoder: unrecognized selector sent to instance.

Huh?

–EDIT–

All right, forget most of that. It seems that I’ve got a bit of a wacko-code-thing going on in my original WhereWasI code which is really making the behaviour odd. I’ll have to get the mess I made a few chapters ago cleaned up …


#14

Remember that only objects that conform to the NSCoding protocol can be archived. MKPlacemark does not conform to this protocol. You’ll have to archive this information by hand (archiving each property of the placemark that you are interested in saving).


#15

Thanks, Joe. It turns out my creation of the MKPlacemark object was superfluous anyway, as the city and province information are stored in the annotation’s subtitle field for my implementation. I spent a few hours this afternoon cleaning up the mess I’d made of WhereWasI the first time through. For some reason unknown even to myself, I’d created instance varables of type MKPlacemark, and whatever type the Geocoder object conforms to, added @property definitions, and @synthesized them, for no purpose other than I guess because I could.

I certainly never used the accessors that were synthesized.

In any event, the code is now a lot more slick, doesn’t drop multiple pins when I add an annotation, and seems to run nice and smooth again, so adding in the file handling implementation should, at this point, be fairly straightforward.

I still plan to create an array of annotations as I drop pins onto the map - the reason being then I could avoid having to filter the annotations array which exists within mapView as was being discussed above - or (and this is my question) is the filtering implementation a better way to go?

Last question - I think it might be beneficial to add in a way to delete annotations from the map by tapping on the pin and then tapping a button which says “delete” which appears within the small popup displaying the annotation data. Can you point me in the right direction on how to accomplish this?

–EDIT–
And with that, we have success. Cleaning up the mess that was the code after the original implementation solved everything. Now to implement a little extra “stuff” for this… deleting the annotations after they’re entered. This could be interesting. Hmm, might have to change the array of annotations I’ve created to a dictionary…


#16

I’m struggling with this Challenge.

At first I tried to mimic the approach taken in Homepwner and created a MapPointStore class but eventually realized that was not necessary.

So then I decided I could simply add the saveChanges and fetchMapPointsIfNecessary methods to the whereamiAppDelegate class.

But I’m being thrown by the fact that in Homepwner we had our own array of Possessions where we could check to see if the array existed and if not try reading Possessions from disk, etc. However in Whereami the array of MapPoints are stored in MKMapView and we can’t create that array ourselves.

How and where are others reading in the stored MapPoints?

Thanks in advance,
mb