reverseGeocode and new CLGeocoder class


#1

Hiya,

I am trying todo the last challenge in the MapKit chapter where we have to reverse geocode a location, I have managed todo this but I have hit a slight snag which is preventing me from completing it. I think I know WHAT it’s doing and WHY, but I am not able to figure out a way to get around it yet. (I am using ARC and iOS5)

The problem occurs when I execute the following code in my init method …

self.city is returning back a null value, I am pretty sure this is because the init method completes WAY before the geocoder can go and do it’s thing.

So my question is, how can I handle this situation? I assume sitting and waiting for the geocoder to complete its work and freezing the app until such time isn’t the way to go?

#import "MapPoint.h"

@implementation MapPoint
@synthesize coordinate, title, dateAdded, subtitle, city;

-(id)initWithCoordinates:(CLLocationCoordinate2D)c 
                   title:(NSString *)t {
    
    self = [super init];
    if (self) {
        
        coordinate = c;
        [self setTitle: t];
        [self setCurrentCity: [[CLLocation alloc] initWithLatitude:c.latitude longitude:c.longitude]];
        
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
        [formatter setTimeStyle:NSDateFormatterNoStyle];
        [formatter setDateStyle:NSDateFormatterLongStyle];
        
        [self setDateAdded: [[NSDate alloc] init]];
        NSString *dateString = [formatter stringFromDate: [self dateAdded]];
        
        [self setSubtitle: [dateString stringByAppendingString: self.city ] ];
        
    }
    return self;
    
}

-(void)setCurrentCity: (CLLocation *)loc {
    
    CLGeocoder *reverseGeo;
    
    if (!reverseGeo) {
        reverseGeo = [[CLGeocoder alloc] init];
    }
    
    [reverseGeo reverseGeocodeLocation: loc completionHandler: 
     ^(NSArray *placemarks, NSError *error) {
         if ([placemarks count] > 0) {
             
             CLPlacemark *topResult = [placemarks objectAtIndex:0];
             self.city = [NSString stringWithFormat:@"%@", [topResult locality]];
             
         }
     }];
}

@end

#2

Hi,

I think your diagnosis is correct so self.city is null when

    [self setSubtitle: [dateString stringByAppendingString: self.city ] ]; is being executed

You could just set city to an empty string - [self setCity:@""]; before that line.

Then some time later the reverseGeocodeLocation completionHandler is going to finish and will set the city to a proper value.
However the subtitle will need to be updated at this point so you will need to add
[self setSubtitle: [subtitle stringByAppendingString: self.city ] ]; to your completionHandler.

I knocked up a quick test to defer the completionHandler for 5 seconds to see what would happen and the map annotation refreshes with the new value on screen - pretty neat.

HTH
Gareth


#3

Thanks for the help Gareth, I did some more investigation prior to your answer and found that indeed I did need to insert my setting of the subtitle into the completionHandler, also I needed to create a for loop which I used to get the result I needed from the Location services, here is my amended code. thanks again.

#import "MapPoint.h"

@implementation MapPoint
@synthesize coordinate, title, dateAdded, subtitle, city, reverseGeo;

-(id)initWithCoordinates:(CLLocationCoordinate2D)c 
                   title:(NSString *)t {
    
    self = [super init];
    if (self) {

        coordinate = c;
        [self setTitle: t];
        [self setCurrentCity: [[CLLocation alloc] initWithLatitude:c.latitude longitude:c.longitude]];
        
        [self setDateAdded: [[NSDate alloc] init]];
        
    }
    return self;
    
}

-(void)setCurrentCity: (CLLocation *)loc {
    
    if (!self.reverseGeo) {
        self.reverseGeo = [[CLGeocoder alloc] init];
    }
    
    [self.reverseGeo reverseGeocodeLocation: loc completionHandler: 
     ^(NSArray *placemarks, NSError *error) {
         
         for (CLPlacemark *placemark in placemarks) {
             
             NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
             [formatter setTimeStyle:NSDateFormatterNoStyle];
             [formatter setDateStyle:NSDateFormatterLongStyle];

             NSString *dateString = [formatter stringFromDate: [self dateAdded]];
             [self setSubtitle: [dateString stringByAppendingString: [placemark locality] ] ];             
         
         }
     }];
}

@end

Richard


#4

With reference to the book implementation, I guess that another possibility would be to call reverseGeocodeLocation at the beginning of foundLocation:(CLLocation*)loc
and then implement the rest of foundLocation (creating MapPoint, adding Annotation and restoring the UI outlets) directly inside the completionHandler: block.
Something along the following lines (in my code, geoState is CLGeocoder instance variable that I allocate at startup):

  • (void)locationFound:(CLLocation*)loc
    {
    [geoState reverseGeocodeLocation:loc completionHandler:^(NSArray *placemarks, NSError *error)
    { //THe following is a debugging line
    NSLog(@“Executing block code into reverseGeoCodeLocation”);

       CLLocationCoordinate2D coord = [loc coordinate];
       
       CLPlacemark *firstElement = [placemarks objectAtIndex:0];
       
       NSString *titleString = [NSString stringWithFormat:@"%@: %@", [locationTitleField text],[firstElement country]];
       
       MapPoint *newPoint = [[MapPoint alloc]initWithCoordinate:coord title:titleString date:[NSDate date]];
       [worldMap addAnnotation];
       
       MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coord, 250, 250);
       [worldMap setRegion:region animated:YES];
       
       [locationTitleField setHidden:NO];
       [activityIndicator stopAnimating];
       [locationTitleField setText:@""];
       [locationManager stopUpdatingLocation];
    

    }];
    }

By doing this the application waits a bit more (usually around 1 second) for Geocoder to complete its task and then writes an updated Annotation and returns the control to the user.
Hope that helps (please note I’m using ARC so you don’t see any release/retain calls in my coding)


#5

@Garethr

Would you have working code for Whereami app with Reverse Geocoding incorporated?

Thanks!


#6

So I thought the easiest way to solve this was to pass the MapPoint being updated into the geocoding method. That way it doesn’t matter what the rest of the app is doing, when there’s a geocoding result ready, the relevant MapPoint will get updated with the info.

So I created a new class, MyGeocoderViewController (this is from the example Apple doco)

[code]#import “MyGeocoderViewController.h”

@implementation MyGeocoderViewController

  • (void)geocodeLocation:(CLLocation *)location forAnnotation:(MapPoint *)annotation
    {
    if (!geocoder)
    geocoder = [[CLGeocoder alloc] init];

    [geocoder reverseGeocodeLocation:location completionHandler:
    ^(NSArray* placemarks, NSError* error){
    if ([placemarks count] > 0)
    {
    CLPlacemark *myloc = [placemarks objectAtIndex:0];
    annotation.subtitle = [NSString stringWithFormat:@"%@, %@", myloc.locality, myloc.administrativeArea];
    }
    }];
    }
    @end
    [/code]

I also added a property for “subtitle” to the MapPoint class - this is part of the MKAnnotation protocol.

Now all we have to do is pass the co-ordinate and MapPoint into geocodeLocation:forAnnotation when we’ve found a location.

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

// Create an instance of MapPoint with the current data
MapPoint *mp = [[MapPoint alloc] initWithCoordinate:coord title:[locationTitleField text]];

// Add it to the map view
[worldView addAnnotation:mp];

// Geocode the location
MyGeocoderViewController *gc = [[MyGeocoderViewController alloc] init];
[gc geocodeLocation:loc forAnnotation:mp];

// MKMapView retains its annotations, we can release
[mp release];
[gc release];

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

[locationTitleField setText:@""];
[activityIndicator stopAnimating];
[locationTitleField setHidden:NO];
[locationManager stopUpdatingLocation];

}
[/code]