问题
I'm new to Objective-C and my C/C++ skills are quite rusty. What better time to learn iOS development(!)
I'm trying to reverse geolocate a position using the CLGeocoder class in iOS. I can successfully get the data I'm interested in (street address) inside the block/callback, however when I try to use that data to populate my variable (outside of the block) the data isn't there. It's as if the object in the block disappears before the MapView object calls it. I'm using __block which as I understand it, should allow the variable to persist outside the block, but it seems not to.
Here's the code in question:
- (void) foundLocation:(CLLocation *)loc
{
CLLocationCoordinate2D coord = [loc coordinate];
// Get our city and state from a reversegeocode lookup and put them in the subtitle field (nowString).
// reversegeocode puts that information in a CLPlacemark object
// First, create the CLGeocoder object that will get us the info
CLGeocoder *geocoder = [[CLGeocoder alloc]init];
// Next create a CLPlacemark object that we can store what reverseGeocodeLocation will give us containing the location data
__block CLPlacemark *placemark = [[CLPlacemark alloc]init];
__block NSString *sPlacemark = [[NSString alloc]init];
// This next bit is where things go awry
[geocoder reverseGeocodeLocation:loc completionHandler:
^(NSArray *placemarks, NSError *error) {
if ([placemarks count] > 0)
{
placemark = [placemarks objectAtIndex:0];// this works!!
sPlacemark = [placemark thoroughfare]; // as does this! I can see the street address in the variable in the debugger.
}
}];
MapPoint *mp = [[MapPoint alloc] initWithCoordinate:coord
title:[locationTitleField text]
subtitle:sPlacemark];
// add it to the map view
[worldView addAnnotation:mp];
// MKMapView retains its annotations, we can release
[mp release];
// zoom region to this location
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coord, 250, 250);
[worldView setRegion:region
animated:YES];
[locationTitleField setText:@""];
[activityIndicator stopAnimating];
[locationTitleField setHidden:NO];
[locationManager stopUpdatingLocation];
}
I haven't completely wrapped my head around 'blocks' so it's likely that's where the problem is, but I can't put my finger on exactly what.
回答1:
So what you have is a timing issue. The call to reverseGeocodeLocation:completionHandler:
is asynchronous. The call itself will return immediately before the actual reverse geolocation happens.
So immediately as the call returns, your method continues, and you're creating an annotation with a placemark that doesn't yet exist.
Then at some point later, your reverse geolocation is finished (remember it requires a network call to a service and all of that). And then after that, your block is fired with the new incoming data.
So by the time your block actually runs, these two local __block variables you created are long gone. Why? Because those variables placemark
and sPlacemark
are local (automatic) variables, local to this method. They come into existence within the method, and then go away when the method is finished. And again, this happens before your block even gets to run. (It turns out the block will write into a copy of each variable later, but that doesn't really matter, because this method is finished by then and you've already tried to read them too early.)
If you put an NSLog message at the end of this method, and another one in your block, you'll see the sequence in which they fire. The order will be the opposite of what you're probably thinking. The one at the end of the method will fire first, and then the one inside the block.
Think of this like a restaurant. The waiter goes back to the kitchen and places an order. Now he doesn't sit there at the kitchen and wait for the food to be cooked, because that takes time. So he leaves the order and continues his work until such time as the order is ready. Now imagine he leaves the order, and then immediately checks the counter. He'll be quite disappointed to see that the food isn't there yet. And that's exactly what you're doing when you immediately try to read the placemark
variable before the cooks have had even a second to cook the order.
So what's the answer? You could create another method that creates the annotation and places it on the map. That method should take the placemark as a parameter, and then you can call that method from within the block (which again is after you actually have the placemark.) Think of this as all the work the waiter would like to do after the order is ready, like take the order to the customer.
There are other ways to do this. But for one single placemark as you show here, that's a simple way to handle this. Obviously, you should also add error handling in case you can't find the placemark, or the service is not available, etc. If the lookup fails, the placemark will be nil and you can check the error for more information.
Hope that helps.
回答2:
Firoze I ran into the same problem. I ended up writing a custom method that took a "completion" block as a param so that when the geocode was finished I would have access to it in that particular block.
-- snippet
- (void)fetchForwardGeocodeAddress:(NSString *)address withCompletionHanlder:(ForwardGeoCompletionBlock)completion {
if (NSClassFromString(@"CLGeocoder")) {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
CLGeocodeCompletionHandler completionHandler = ^(NSArray *placemarks, NSError *error) {
if (error) {
NSLog(@"error finding placemarks: %@", [error localizedDescription]);
}
if (placemarks) {
[placemarks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
CLPlacemark *placemark = (CLPlacemark *)obj;
NSLog(@"PLACEMARK: %@", placemark);
if ([placemark.country isEqualToString:@"United States"]) {
NSLog(@"********found coords for zip: %f %f", placemark.location.coordinate.latitude,placemark.location.coordinate.longitude);
if (completion) {
completion(placemark.location.coordinate);
}
*stop = YES;
}
}];
}
};
[geocoder geocodeAddressString:address completionHandler:completionHandler];
} else {
/**
* Since the request to grab the geocoded address is using NSString's initWithContentsOfURL
* we are going to make use of GCD and grab that async. I didn't put this in the
* method itself because there could be an instance where you would want to make a
* synchronous call. Better to have flexibility.
*
* Possible improvement could be to write another method that does the async
* loading for you. However, if you did it that way how would you be notified
* when the results returned. Possibly KVO?
*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
CLLocation *location = [address newGeocodeAddress];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(location.coordinate);
}
});
});
}
}
In my particular use case I had to support both iOS4 and iOS5, hence the check and extra logic. I give a little more detail on my blog about it: http://blog.corywiles.com/forward-geocoding-in-ios4-and-ios5
来源:https://stackoverflow.com/questions/8697177/objective-c-blocks-variables-and-clgeocoder-and-or-clplacemark