Reverse geolocation synchronization in UITableViewCell - ios

I need to convert my latitude and longitude, received from the server to location. As I user Parse API I don't find that's a good idea to convert them in viewDidLoad, so I decided to make it in cellForRowAtIndexPath.
I tried this approach: Synchronization of concurrent processes execution
Here:
...
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.geocoder reverseGeocodeLocation: [[CLLocation alloc] initWithLatitude:meeting.location.latitude
longitude:meeting.location.longitude] completionHandler:
^(NSArray *placemarks, NSError *error) {
CLPlacemark *placemark = [placemarks objectAtIndex:0];
meeting.location.displayName = [placemark.addressDictionary objectForKey:#"Street"];
NSLog(#"%#", meeting.location.displayName);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
cell.subInfo.text = meeting.location.displayName;
return cell;
But it seems to hang endlessly. Where am I wrong?
UPD: I've tried to move all this stuff back to viewDidLoad. In my for loop, where I parse downloaded from Parse.com information I now have:
...
_location.latitude = ((PFGeoPoint *)entry[#"location"]).latitude;
_location.longitude = ((PFGeoPoint *)entry[#"location"]).longitude;
_semaphore = dispatch_semaphore_create(0);
[self reverseGeocode:_location];
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
...
Where _semaphore is #property (strong) dispatch_semaphore_t semaphore;
reverseGeocode::
[self.geocoder reverseGeocodeLocation: [[CLLocation alloc] initWithLatitude:meetingLocation.latitude
longitude:meetingLocation.longitude] completionHandler:
^(NSArray *placemarks, NSError *error) {
CLPlacemark *placemark = [placemarks objectAtIndex:0];
meetingLocation.displayName = [placemark.addressDictionary objectForKey:#"Street"];
dispatch_semaphore_signal(_semaphore);
}];
Now it seems to hang, but I have no idea where, because it doesn't even enter the geocoded completionHandler block.

You tried to get the value for meeting.location.displayName in the block, and then set cell.subInfo.text with the value of meeting.location.displayName after the block. The problem will be the line cell.subInfo.text = meeting.location.displayName; can be executed before the block is returned. You can try to put the line cell.subInfo.text = meeting.location.displayName; inside the block.
But I don't see why you want to reverse geolocation in this method, the reverse function may be called every time when the user scroll to particular if you don't set the condition when to do reversion. If viewDidLoad is not a good idea for your design, try viewWillAppear.

Related

Storing the values from completion handler into a variable

I am trying to store a String Value inside the completion handler but its scope is restricted to that block only. How to resolve it ?
// Do any additional setup after loading the view, typically from a nib.
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
locationManager.distanceFilter = kCLDistanceFilterNone;
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
[locationManager startUpdatingLocation];
CLGeocoder *geocoder = [[CLGeocoder alloc] init] ;
NSString *co;
[geocoder reverseGeocodeLocation:locationManager.location
completionHandler:^(NSArray *placemarks, NSError *error) {
NSLog(#"reverseGeocodeLocation:completionHandler: Completion Handler called!");
if (error){
NSLog(#"Geocode failed with error: %#", error);
return;
}
CLPlacemark *placemark = [placemarks objectAtIndex:0];
NSLog(#"placemark.country %#",placemark.country);
co = placemark.country;
//
}];
NSLog(#"%#",co);
At this line the value of co becomes null again. Please let me know, how can retain the value outside the completion handler, which I store inside completion handler.
The issue isn't a matter of scope, it's that the log is called before the completion block. The reverse geocode call is asynchronous. It will return the block whenever it's done with what it's doing, but in the meantime, the rest of your method will execute. If you print co the line after you set its value but within the completion block, it will display the correct value.
Example:
[geocoder reverseGeocodeLocation:locationManager.location
completionHandler:^(NSArray *placemarks, NSError *error) {
NSLog(#"reverseGeocodeLocation:completionHandler: Completion Handler called!");
if (error){
NSLog(#"Geocode failed with error: %#", error);
return;
}
CLPlacemark *placemark = [placemarks objectAtIndex:0];
NSLog(#"placemark.country %#",placemark.country);
co = placemark.country;
// The completion block has returned and co has been set. The value equals placemark.country
NSLog(#"%#",co);
}];
// This log is executed before the completion handler. co has not yet been set, the value is nil
NSLog(#"%#",co);
If you need to use the co variable outside of the block, you should call the methods it will be used in from within the completion block:
[geocoder reverseGeocodeLocation:locationManager.location
completionHandler:^(NSArray *placemarks, NSError *error) {
[self myMethodWithCountry:placemark.country];
}];
- (void)myMethodWithCountry:(NSString *)country {
// country == placemark.country
}
The NSLog command you wrote will work before you complete the block. Since this happens you will get null. One thing you can do is print the value of co inside the block instead of doing it outside.
OR
change the declaration of co as follows:
__block NSString *co= nil;
As Julie suggested, add __block before NSString *co; i.e. __block NSString *co;. That's two underscores btw.

reverseGeocodeLocation Only Executes Completion Block Once

I have an array of coordinates that I step through with a for loop. I would like to place annotations on a map for each location and have the subtitle for the callout be the address of the coordinate, found by using reverseGeocodeLocation In the for loop, I call the reverseGeocodeLocation method, and inside the completion block I create the annotation and display it on the map. However, when I run the app, only one annotation shows up. I went in the debugger, and the completion block is only getting called once (for two calls to the reverseGeocodeLocation method). Any suggestions to fix this?
My for loop:
for(int i = 0; i < [locations count]; i++)
{
CLLocation *location = [locations objectAtIndex:i];
__block NSString *info;
NSLog(#"Resolving the Address");
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error)
{
NSLog(#"Found placemarks: %#, error: %#", placemarks, error);
if (error == nil && [placemarks count] > 0)
{
placemark = [placemarks lastObject];
info = [NSString stringWithFormat:#"%# %# %# %#, %#",
placemark.subThoroughfare, placemark.thoroughfare,
placemark.postalCode, placemark.locality,
placemark.administrativeArea];
[self remainderOfMethod:location withAddress:info atIndex:i];
}
else
{
NSLog(#"%#", error.debugDescription);
}
} ];
}
And the method called at the completion block:
- (void) remainderOfMethod: (CLLocation *)location withAddress:(NSString *)info atIndex: (int)i
{
MKPointAnnotation* annotation = [[MKPointAnnotation alloc] init];
if (location != nil)
{
[annotation setSubtitle:[NSString stringWithFormat:#"%#", info]];
annotation.coordinate = location.coordinate;
[self.mapView addAnnotation:annotation];
}
}
Thanks!
From the official Apple documentation:
After initiating a reverse-geocoding request, do not attempt to
initiate another reverse- or forward-geocoding request
You can find the docs here: https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLGeocoder_class/Reference/Reference.html#//apple_ref/occ/instm/CLGeocoder/reverseGeocodeLocation:completionHandler:
One way to solve it is to do only one request at a time in a recursive method that pops a location from a stack (or array) on each iteration.
Even in that case, consider what Apple has to say about it:
Geocoding requests are rate-limited for each app, so making too many
requests in a short period of time may cause some of the requests to
fail
So you may want to request geocoding on demand, for example when a user taps on an annotation.

block until reverseGeocode has returned

I am trying to find a user's location from a coordinate to save into my database.
To find the location name I am using reverseGeocode. However since it is a block method my self.locationName will return (and save as nil) into the database. So I have tried to find a solution to the problem, and tried to put together the following solution using semaphores to try and block until I get a locationName I can save, but the app just hangs when the save button is pressed. Should I even be going about this problem in this way or is there a better way?
dispatch_semaphore_t semaphore;
- (void)reverseGeocode:(CLLocation *)location {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
NSLog(#"Finding address");
if (error) {
NSLog(#"Error %#", error.description);
} else {
CLPlacemark *placemark = [placemarks lastObject];
self.locationName = [NSString stringWithFormat:#"%#", ABCreateStringWithAddressDictionary(placemark.addressDictionary, NO)];
dispatch_semaphore_signal(semaphore);
}
}];
}
-(NSString *)findLocation:(CLLocation *)startingLocation
{
semaphore = dispatch_semaphore_create(0);
[self reverseGeocode:startingLocation];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //should maybe timeout
return self.locationName;
}
You are thinking about this all wrong. That is not how asynchronous code works. Do NOT block until the code returns. Just launch the code to start the reverse geocode, and finish up. Now when the reverse geocode finishes, it calls you back and you can do whatever you want with the returned info. That is the whole point of the completion handler: it doesn't run until the reverse geocode has completed.
Just get rid of the semaphores and let things happen asynchronously. Here is a complete example without the secondary method:
CLLocation* loc = userLocation.location;
[geo reverseGeocodeLocation:loc
completionHandler:^(NSArray *placemarks, NSError *error)
{
if (placemarks) {
CLPlacemark* p = [placemarks objectAtIndex:0];
NSLog(#"%#", p.addressDictionary); // do something with address
}
}];
As you've already been told, if you really want to call this from another method and then do something further, then pass a block to this method and call the block inside the completion handler. That means the block you passed is code that will run when the geocoding has completed, which is exactly what you want - without semaphores and without freezing the app.
Freezing the app is bad form and the WatchDog will kill your app dead if you do it for too long. Just don't do it.

Objective-C Return Method-Declared Object From Nested Block

In the following block of code, the first method calls the second method, which should return the results of a geocoding process:
- (void)foo {
CLPlacemark *currentMark = [self reverseGeocodeLocation:locationManager.location];
}
- (CLPlacemark *)reverseGeocodeLocation:(CLLocation *)location {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
__block CLPlacemark *placeMark;
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
if (!error) {
if ([placemarks firstObject])
placeMark = [placemarks firstObject];
}
}];
return placeMark;
}
However, since the program's execution, doesn't wait for the geocoding to complete before continuing (hence the completion block), there is always a danger that the placeMark variable will be returned uninstantiated before the geocoding process finishes and the completion block is called. I've faced the same predicament when making HTTP requests to web services whose results will not return for an indeterminate amount of time.
The only solution I've seen thus far is to nest the all the code from foo in the geocoder's completion block, which quickly becomes very ugly and difficult to maintain.
What is the best way for the currentMark variable in foo to be set to the result of the second method's completion block without nesting it in the block?
Instead of having the function return a value, just add a callback block to return the value.
Try this:
- (void)reverseGeocodeLocation:(CLLocation *)location withCallback:(void(^)(CLPlacemark *resultPlacemark, NSError *error))_block {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
_block([placemark objectAtIndex:0], error);
}];
}
and then foo would be
- (void)foo {
__block CLPlacemark *currentMark;
[self reverseGeocodeLocation:(CLLocation *)location withCallback:(CLPlacemark *mark, NSError *error) {
currentMark = mark;
}];
}
It seems that the best way to handle this issue overall is to use delegates. That way, the flow of the program isn't obstructed by waiting an indeterminate amount of time for the completion block to return.
Here's a short example of what I've decided on:
- (void)reverseGeocodeLocation:(CLLocation *)location {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
if (!error) {
if ([placemarks firstObject])
[delegate performSelectorOnMainThread:#selector(done:) withObject:[placemarks firstObject];
}
}];
return placeMark;
}
In hindsight, this actually seems rather elegant. The control flow of the main thread (i.e. UI presentation), isn't hindered in any way, and the view controller querying data can essentially be "notified" that data has loaded, rather than directly demanding a returned value.

Synchronization of concurrent processes execution

I have some concurrent operation for the data processing. During the processing I need to retrieve the reverse geocoding for the location. It is known that - (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler also performs geocoding request in background thread and returns immediately after the call. When geocoded finishes request it executes the completion handler on the main thread. How can I block my concurrent operation until the geocoder retrieves the result?
__block CLPlacemark *_placemark
- (NSDictionary *)performDataProcessingInCustomThread
{
NSMutableDictionary *dict = [NSMutableDictionary alloc] initWithCapacity:1];
// some operations
CLLocation *location = [[CLLocation alloc] initWithLatitude:40.7 longitude:-74.0];
[self proceedReverseGeocoding:location];
// wait until the geocoder request completes
if (_placemark) {
[dict setValue:_placemark.addressDictionary forKey:#"AddressDictionary"];
return dict;
}
- (void)proceedReverseGeocoding:(CLLocation *)location
{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
if ([error code] == noErr) {
_placemark = placemarks.lastObject;
}
}];
}
To achieve this we can use dispatch_semaphore_t. First of all we need to add semaphore to the class:
dispatch_semaphore_t semaphore;
When we are processing data and ready to receive the geocoding data, we should create dispatch semaphore and start to wait a signal:
semaphore = dispatch_semaphore_create(0);
[self proceedReverseGeocoding:location];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
At the end of CLGeocodeCompletionHandler all we need is to send the signal to resume the data processing:
dispatch_semaphore_signal(semaphore);
After this the data processing will continue

Resources