Gold Challenge Solution: Concise Coordinates


#1

Did I implement NSKeyedArchiver and NSKeyedUnarchiver properly for this solution?

Added NSCoding methods to BNRMapPoint.m:

[code]- (void)encodeWithCoder:(NSCoder *)aCoder
{
double latitude = coordinate.latitude;
double longitude = coordinate.longitude;

[aCoder encodeDouble:latitude forKey:@"latitude"];
[aCoder encodeDouble:longitude forKey:@"longitude"];

}

  • (id)initWithCoder:(NSCoder *)aDecoder
    {
    self = [super init];
    if (self) {
    double latitude = [aDecoder decodeDoubleForKey:@“latitude”];
    double longitude = [aDecoder decodeDoubleForKey:@“longitude”];
    coordinate = CLLocationCoordinate2DMake(latitude, longitude);
    }
    return self;
    }[/code]
    Added static variable for the preference:

Archived the coordinate in foundLocation:

[code]NSNumber *latitudeObject = [NSNumber numberWithDouble:coord.latitude];
NSNumber *longitudeObject = [NSNumber numberWithDouble:coord.longitude];
NSArray *coordinateArray = [NSArray arrayWithObjects:latitudeObject, longitudeObject, nil];

[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:coordinateArray]
forKey:WhereamiCoordinatePrefKey];[/code]
Unarchived the coordinate in viewDidLoad:[code]
NSArray *coordinateArray = [NSKeyedUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults]
objectForKey:WhereamiCoordinatePrefKey]];

CLLocationCoordinate2D savedCoordinate = CLLocationCoordinate2DMake([[coordinateArray objectAtIndex:0] doubleValue],
[[coordinateArray objectAtIndex:1] doubleValue]);

MKCoordinateRegion savedRegion = MKCoordinateRegionMakeWithDistance(savedCoordinate, 250, 250);
[worldView setRegion:savedRegion animated:YES];[/code]


#2

I did things a bit differently : I understood that you can neither add a structure to NSUserDefaults nor archive a structure.
In a prevous challenge (chapter 14), we split the CLLocationCoordinate2D structure into latitude and longitude floats and archived these.
In the silver challenge, we split the CLLocationCoordinate2D structure into latitude and longitude floats and used these coordinates as NSUserDefaults values.
In this challenge, I tried to save the CLLocation object directly, since NSKeyedArchiver accepts Objects.

First, I defined an Archive Path:

[code]- (NSString *)itemArchivePath
{
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

// Get one and only document directory from that list
NSString *documentDirectory = [documentDirectories objectAtIndex:0];

return [documentDirectory stringByAppendingPathComponent:@"location.archive"];

}[/code]

In mapView:didUpdateUserLocation:, I wanted to use u as the archive object, but it’s a MKUserLocation, not a CLLocation … so I created a CLLocation based on the coordinates of the MKUserLocation u, and I archived the coordinates of this CLLocation. Maybe there’s a better way to do that.

[code]- (void)mapView:(MKMapView *)mv didUpdateUserLocation:(MKUserLocation *)u
{
// Archive the current location
NSString *path = [self itemArchivePath];
CLLocation *currentLoc = [[CLLocation alloc] initWithLatitude:u.coordinate.latitude longitude:u.coordinate.longitude];
BOOL success = [NSKeyedArchiver archiveRootObject:currentLoc toFile:path];
if (success) {
NSLog(@“Saved latitude %f - Saved Longitude %f”, u.coordinate.latitude, u.coordinate.longitude);
} else {
NSLog(@“Unable to save current location”);

}

CLLocationCoordinate2D loc = [u coordinate];

MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(loc, 250, 250);
[worldView setRegion:region animated:YES];

}
[/code]

Then, I load this archive object during ViewDidLoad:
I included a default location there too, if no previously saved location can be loaded.

[code]- (void)viewDidLoad
{
[worldView setShowsUserLocation:YES];

NSInteger mapTypeValue = [[NSUserDefaults standardUserDefaults]
                          integerForKey:WhereamiMapTypePrefKey];

// Update the UI
[mapTypeControl setSelectedSegmentIndex:mapTypeValue];

// Update the map
[self changeMapType:mapTypeControl];


// Update the location with the last saved coordinates    
NSString *path = [self itemArchivePath];
CLLocation *loc = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

if (!loc) {
    CLLocationDegrees defaultLatitude = 37.331789;
    CLLocationDegrees defaultLongitude = -122.029620;
    loc = [[CLLocation alloc] initWithLatitude:defaultLatitude longitude:defaultLongitude];
}
CLLocationCoordinate2D savedLocation = [loc coordinate];
NSLog(@"Loading latitude %f - Longitude %f", [loc coordinate].latitude, [loc coordinate].longitude);

// Zoom the region to this location
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(savedLocation, 250, 250);
[worldView setRegion:region animated:YES];

}
[/code]

Apparently, it works …


#3

Alright, I’ve already spent too much time on this. :slight_smile:

My question for Bryan is: did it work for you? If so, then, YES!, I’d say you did it right. As outlined below, it didn’t work for me.

I’m completely baffled. Like you, Bryan, I thought the challenge was saving it into defaults but I went about it a little bit differently. Since CLLocation conforms to NSCoding I went ahead and tried to save off that object. Here is what I put in initialize:, for example.

    CLLocation *loc = [[CLLocation alloc] initWithLatitude:-90.360008 longitude:38.580001];
    NSData *locationData = [NSKeyedArchiver archivedDataWithRootObject:loc];
    [defaults setValue:locationData forKey:WhereamiLocationPrefKey];

I also did something similar in foundLocation: (although it never got called, as described below):

    NSData *locationData = [NSKeyedArchiver archivedDataWithRootObject:loc];
    [[NSUserDefaults standardUserDefaults] setValue:locationData forKey:WhereamiLocationPrefKey];

My problems occurred in viewDidLoad: although I have no idea why. Here is the code there:

    NSData *locData = [[NSUserDefaults standardUserDefaults] valueForKey:WhereamiLocationPrefKey];
    CLLocation *startingLoc = [NSKeyedUnarchiver unarchiveObjectWithData:locData];
    NSLog(@"starting Loc:  %@", startingLoc);
    CLLocationCoordinate2D savedLocation = [startingLoc coordinate];
    MKCoordinateRegion savedRegion = MKCoordinateRegionMakeWithDistance(savedLocation, 250, 250);   
    [worldView setRegion:savedRegion animated:YES];

The app crapped out at the last line below, setRegion:animated:, and I couldn’t figure out why. When NSLog prints out in that section of code, it looks good (with the proper latitude, longitude and such), but the app just hangs at the last line of code in the section above. I don’t know enough to figure out why at this point.


#4

EDIT: The challenge contradicts itself on the point if you are supposed to use NSUserDefaults or not, it says “save it under one key” but then says “Don’t use setObject: forKey:”, feels more natural to use the NSUserDefaults considering what the chapter was about.

@drabenau I did nearly exactly as you did so I can’t see why it didn’t work

Here is my code if it is any help

  • (void) Initialise
CLLocation *loc = [[CLLocation alloc] initWithLatitude:42.454859 longitude:-76.477504];
    [defaults setObject:loc forKey:WhereamiCoordPrefKey];
    
    [[NSUserDefaults standardUserDefaults] registerDefaults];
}

-(void) viewDidLoad

[code]
NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:WhereamiCoordPrefKey];
CLLocation *Loc = [NSKeyedUnarchiver unarchiveObjectWithData:data];
CLLocationCoordinate2D coord = Loc.coordinate;

MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coord, 250, 250);
NSLog(@"Adding this coord to map %f, %f", coord.latitude, coord.longitude);
    
[worldView setRegion:region animated:YES];

}[/code]

-(void) mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation

[code]NSLog(@“Saving new coord in default: %f, %f”, center.latitude, center.longitude);

CLLocation *location = [[CLLocation alloc] initWithLatitude:center.latitude longitude:center.longitude];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject];

[[NSUserDefaults standardUserDefaults] setObject:data 
                                          forKey:WhereamiCoordPrefKey];

}[/code]


#5

@drabenau My solution worked but I like your and FreddyF’s approach more. The CLLocation class is NSCoding compliant so it is simpler to create a CLLocation object and then archive/unarchive it.

One way to do this would be to create a CLLocation property in the BNRMapPoint class:

Set the CLLocation object using the internal coordinates from coordinate.latitude/longitude. Then encode/decode it:

[code]- (void)encodeWithCoder:(NSCoder *)aCoder
{
locationObject = [[CLLocation alloc] initWithLatitude:coordinate.latitude longitude:coordinate.longitude];
[aCoder encodeObject:locationObject forKey:@“locationObject”];
}

  • (id)initWithCoder:(NSCoder *)aDecoder
    {
    self = [super init];
    if (self) {
    locationObject = [aDecoder decodeObjectForKey:@“locationObject”];
    }
    return self;
    }
    [/code]
    In WhereamiViewController, make a BNRMapPoint property (mine is “mp”). Archive the BNRMapPoint object in the saveChanges method inside WhereamiViewController.m.

- (void)saveChanges { NSString *path = [self itemArchivePath]; [NSKeyedArchiver archiveRootObject:mp toFile:path]; }
The saveChanges method could be called inside foundLocation: when the user enters a location title in the text field. In foundLocation:

mp = [[BNRMapPoint alloc] initWithCoordinate:coord title:[locationTitleField text]]; [self saveChanges];
Unarchive the BNRMapPoint object and retrieve the CLLocation object from the locationObject property in viewDidLoad:

NSString *path = [self itemArchivePath]; mp = [NSKeyedUnarchiver unarchiveObjectWithFile:path]; CLLocation *savedLocation = [mp locationObject]; CLLocationCoordinate2D savedCoordinate = [savedLocation coordinate];


#6

Like others in this thread, I thought that the aims of this challenge were a bit less clear than many others.

Anyway, I did this completely within the confines of the WhereamiViewController class.

At first I tried to just throw the CLLocation object into an NSUserDefaults, but that crashed, so I had to go down the same track as others here, and first put the CLLocation object into an NSKeyedArchiver object, and then put THAT into the NSUserDefaults, so here’s my initialize:

[code]NSString * const WhereamiMapTypePrefKey =
@“WhereamiMapTypePrefKey”;

NSString * const WhereamiLocationPrefKey =
@“WhereamiLocationPrefKey”;

@implementation WhereamiViewController

+(void)initialize
{
CLLocation *locationDefault = [[CLLocation alloc]
initWithLatitude:-38.2f
longitude:144.5f]; // btw that’s near Melbourne, Australia :slight_smile:
NSData *locDefData = [NSKeyedArchiver archivedDataWithRootObject:locationDefault];
NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:1],WhereamiMapTypePrefKey,
locDefData,WhereamiLocationPrefKey,nil];
[[NSUserDefaults standardUserDefaults] registerDefaults];
NSLog(@“The registration domain defaults are %@”, [[NSUserDefaults standardUserDefaults] dictionaryRepresentation]);
}[/code]

And then my foundLocation (with probably too many redundant NSLog’s :slight_smile: ):

[code]-(void)foundLocation:(CLLocation *)loc {
CLLocationCoordinate2D coord = [loc coordinate];

// creat an instance of BNRMapPoint with the current data
BNRMapPoint *mp = [[BNRMapPoint alloc] initWithCoordinate:coord
                                                    title:[locationTitleField text]];
// Add it to the map view
[worldView addAnnotation:mp];

// Zoom the region to this location
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coord, 500, 500);
[worldView setRegion:region animated:YES];

// reset the UI
[locationTitleField setText:@""];
[activityIndicator stopAnimating];
[locationTitleField setHidden:NO];
[locationManager stopUpdatingLocation];

// Now set up the location data into a single NSUserDefaults item
// but having to use NSData as an interim step
CLLocation *locationNew = [[CLLocation alloc]
    initWithLatitude:coord.latitude
           longitude:coord.longitude];
NSData *locationData = [NSKeyedArchiver archivedDataWithRootObject:locationNew];
[[NSUserDefaults standardUserDefaults] setObject:locationData forKey:WhereamiLocationPrefKey];

// Now retrieve what we've just put there... to verify it's there
NSData *locationDataForNSLogging = [[NSUserDefaults standardUserDefaults] objectForKey:WhereamiLocationPrefKey];
CLLocation *locationForNSLogging = [NSKeyedUnarchiver unarchiveObjectWithData:locationDataForNSLogging];
NSLog(@"And, in the CLLocation object format [%.2f, %.2f]", locationForNSLogging.coordinate.latitude,locationForNSLogging.coordinate.longitude);

}
[/code]

And then my viewDidLoad:

[code]-(void)viewDidLoad{
[worldView setShowsUserLocation:YES];

//NSLog(@"At the start of viewDidLoad: [%.2f, %.2f]", [[NSUserDefaults standardUserDefaults] doubleForKey:WhereamiLatitudePrefKey], [[NSUserDefaults standardUserDefaults] doubleForKey:WhereamiLongitudePrefKey]);

// Retrieve the last-set MapType
NSInteger mapTypeValue = [[NSUserDefaults standardUserDefaults] integerForKey:WhereamiMapTypePrefKey];
NSLog(@"The mapTypeValue is %d, the WhereamiMapTypePrefKey is %@", mapTypeValue, WhereamiMapTypePrefKey);

// Update the UI
mapTypeControl.selectedSegmentIndex = mapTypeValue;
[self changeMapType:mapTypeControl];

// Now get the location data NSUserDefaults
NSData *retrieveLocData = [[NSUserDefaults standardUserDefaults] objectForKey:WhereamiLocationPrefKey];
CLLocation *retrievedLocation = [NSKeyedUnarchiver unarchiveObjectWithData:retrieveLocData  ];

// Redraw the map centered on this location
[self foundLocation:retrievedLocation];

}
[/code]


#7

My solution is a bit different in that I encoded the BNRMapPoint itself. I picked up on some clues from the solutions provided in this thread and also from the Apple documentation. My goals were to:
[ul]use NSUserDefaults
use NSKeyedArchiver
DO NOT use NSUserDefaults’ setObject:forKey:[/ul]
I achieved the first 2 goals but not the last.

I got reading the “Archives and Serializations Programming Guide” which told me that “user preferences are stored as property lists”. In the “Introduction to Property Lists” page I read a note that its limitation would seem to prevent User Defaults of types like NSColor and such. But if the objects conform to NSCoding protocol they can be archived to NSData objects which are property list compatible. So that’s what I wanted to do. There was even an example referenced under “Storing NSColor in User Defaults”.

I would make BNRMapPoint extend NSCoding so that it could be archived as an NSData object and registered as a user default. The only weird thing is that the function to register an NSData object as a user default is setObject:forKey: which the hint said we would not use. Hmmph. Oh well.

BNRMapPoint.h

[code]@interface BNRMapPoint : NSObject <MKAnnotation, NSCoding>

  • (id)initWithCoordinate:(CLLocationCoordinate2D)c Title:(NSString *)t;

@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property (nonatomic, copy) NSString *title;
[/code]

in BNRMapPoint.m
In InitWithCoder: I got a crash when setting the BNRMapPoint’s coordinate property using [self setCoordinate]. Setting coordinate directly worked instead.?

[code]- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
[self setTitle:[aDecoder decodeObjectForKey:@“title”]];

    CLLocationDegrees lat = [aDecoder decodeDoubleForKey:@"latitude"];
    CLLocationDegrees lon = [aDecoder decodeDoubleForKey:@"longitude"];
    
    coordinate = CLLocationCoordinate2DMake(lat, lon);
    //[self setCoordinate:CLLocationCoordinate2DMake(lat, lon)];  // Caused crash. Replaced with line above. WHY???
}
return self;

}

  • (void)encodeWithCoder:(NSCoder *)aCoder
    {
    [aCoder encodeDouble:coordinate.latitude forKey:@“latitude”];
    [aCoder encodeDouble:coordinate.longitude forKey:@“longitude”];
    [aCoder encodeObject:title forKey:@“title”];
    }
    [/code]

in WhereamiViewController.m

NSString * const WhereamiMapTypePrefKey = @"WhereamiMapTypePrefKey";
NSString * const WhereamiLastLocationPrefKey = @"WhereamiLastLocationPrefKey";

+ (void)initialize
{
    NSNumber *defaultMapType = [NSNumber numberWithInt:1];
    CLLocationCoordinate2D defLoc = CLLocationCoordinate2DMake(33.3, -111.1);
    BNRMapPoint *defMapPt = [[BNRMapPoint alloc] initWithCoordinate:defLoc Title:@"Current"];
    NSData *defData = [NSKeyedArchiver archivedDataWithRootObject];
    NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:defData, WhereamiLastLocationPrefKey, defaultMapType, WhereamiMapTypePrefKey, nil];
    
    [[NSUserDefaults standardUserDefaults] registerDefaults];
    NSLog(@"initialize");
    NSLog(@"%@, %f, %f", defaultMapType, [defMapPt coordinate].latitude, [defMapPt coordinate].longitude);
}

- (void)foundLocation:(CLLocation *)loc
{
    CLLocationCoordinate2D coord = [loc coordinate];

    NSLog(@"foundLocation");
    NSLog(@"%f, %f", coord.latitude, coord.longitude);
    
    // Create an instance of BNRMapPoint with the current data
    BNRMapPoint *newLocation = [[BNRMapPoint alloc] initWithCoordinate:coord
                                                        Title:[locationTitleField text]];
    // Save User Data
    NSData *defaultLoc = [NSKeyedArchiver archivedDataWithRootObject:newLocation];
    [[NSUserDefaults standardUserDefaults] setObject:defaultLoc forKey:WhereamiLastLocationPrefKey];
    
    // Add it to the map view
    [worldView addAnnotation:newLocation];
    
    // Zoom the map to this region
    MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coord, 250, 250);
    [worldView setRegion:region animated:YES];
}

- (void)viewDidLoad
{
     NSInteger mapTypeValue = [[NSUserDefaults standardUserDefaults]
                              integerForKey:WhereamiMapTypePrefKey];
    
    BNRMapPoint *defaultMapPt = nil;
    NSData *defaultLoc = [[NSUserDefaults standardUserDefaults] dataForKey:WhereamiLastLocationPrefKey];
    if (defaultLoc != nil) {
        defaultMapPt = (BNRMapPoint *)[NSKeyedUnarchiver unarchiveObjectWithData:defaultLoc];
    }
    NSLog(@"viewDidLoad");
    NSLog(@"%d, %f, %f", mapTypeValue, [defaultMapPt coordinate].latitude, [defaultMapPt coordinate].longitude);
    
    // Zoom the map to this region
    MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance([defaultMapPt coordinate], 250, 250);
    [worldView setRegion:region animated:YES];

    // Update the UI
    [mapTypeControl setSelectedSegmentIndex:mapTypeValue];
    
    // Update the map
    [self changeMapType:mapTypeControl];
}

Tests out OK, so onto the next Chapter of this great book. I’ll check back in here to see if there’s any further insight or explanation on the Hint and expected solution to this challenge.


#8

Hello,

I am a little bit stuck with this challenge, so help appreciated.
Even though I correctly retrieve the coordinate that was saved in the NSUserDefaults in viewDidLoad
(I also try to show it using foundLocation method and passing this coordinate-also in viewDidLoad), still
my map shows my current location.

I think this happens due to this call(which is usually at the beginning of viewDidLoad):

Did you also encounter such issue? How did you solve it? Thanks.


#9

I’ve seen several ways to complete this challenge so thought I’d add in the way I did it with encoding and decoding in BNRMapPoint.m:

[code]- (void)encodeWithCoder:(NSCoder *)encoder {
NSData *data = [NSData dataWithBytes:&coordinate length:sizeof(coordinate)];
[encoder encodeObject:data forKey:@“storedCoordinate”];
}

  • (id)initWithCoder:(NSCoder *)decoder {
    self = [super init];
    if (self) {
    NSData *data = [decoder decodeObjectForKey:@“storedCoordinate”];
    [data getBytes:&coordinate length:sizeof(coordinate)];
    }
    return self;
    }[/code]