Clgeocoder in Swift - Unable to Return String When Using Reversegeocodelocation

CLGeocoder in Swift - unable to return string when using reverseGeocodeLocation

Since reverseGeocodeLocation is an asynchronous function, you need to make your getPlaceName function pass the answer back via a block instead of a return statement. Example:

func getPlaceName(latitude: Double, longitude: Double, completion: (answer: String?) -> Void) {

let coordinates = CLLocation(latitude: latitude, longitude: longitude)

CLGeocoder().reverseGeocodeLocation(coordinates, completionHandler: {(placemarks, error) -> Void in
if (error != nil) {
println("Reverse geocoder failed with an error" + error.localizedDescription)
completion(answer: "")
} else if placemarks.count > 0 {
let pm = placemarks[0] as CLPlacemark
completion(answer: displayLocaitonInfo(pm))
} else {
println("Problems with the data received from geocoder.")
completion(answer: "")
}
})

}

Unable Return String from CLGeocoder reverseGeocodeLocation

The problem is that it runs asynchronously, so you can't return the value. If you want to update some property or variable, the right place to do that is in the closure you provide to the method, for example:

var geocodeString: String?

location.reverseGeocodeLocation { answer in
geocodeString = answer
// and trigger whatever UI or model update you want here
}

// but not here

The entire purpose of the closure completion handler pattern is that is the preferred way to provide the data that was retrieved asynchronously.

can someone explain why i can't return a value from this method?

As pointed out here, you have to add a completion handler to your method:

func getLocation(completion: @escaping (String) -> Void) {

longitude = (locationManager.location?.coordinate.longitude)!
latitude = (locationManager.location?.coordinate.latitude)!


let location = CLLocation(latitude: latitude, longitude: longitude)
print(location)

CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)

if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}

if placemarks!.count > 0 {
let pm = placemarks![0]
print("locality is \(pm.locality)")
completion(pm.locality!)
}
else {
print("Problem with the data received from geocoder")
}
})

}

And then just do:

getLocation() {
locality in
self.city = locality
}

CLGeocoder() returns nil unexpectedly

As Leo said, you don’t want to run the requests concurrently. As the documentation says:

After initiating a reverse-geocoding request, do not attempt to initiate another reverse- or forward-geocoding request. 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. When the maximum rate is exceeded, the geocoder passes an error object with the value CLError.Code.network to your completion handler.

There are a few approaches to make these asynchronous requests run sequentially:

  1. The simple solution is to make the method recursive, invoking the next call in the completion handler of the prior one:

    func retrievePlacemarks(at index: Int = 0) {
    guard index < locations.count else { return }

    lookUpCurrentLocation(location: locations[index]) { name in
    print(name ?? "no name found")
    DispatchQueue.main.async {
    self.retrievePlacemarks(at: index + 1)
    }
    }
    }

    And then, just call

    retrievePlacemarks()

    FWIW, I might use first rather than [0] when doing the geocoding:

    func lookUpCurrentLocation(location: CLLocation, completionHandler: @escaping (String?) -> Void) {
    CLGeocoder().reverseGeocodeLocation(location) { placemarks, _ in
    completionHandler(placemarks?.first?.name)
    }
    }

    I don’t think it’s possible for reverseGeocodeLocation to return a non-nil, zero-length array (in which case your rendition would crash with an invalid subscript error), but the above does the exact same thing as yours, but also eliminates that potential error.

  2. An elegant way to make asynchronous tasks run sequentially is to wrap them in an asynchronous Operation subclass (such as a general-purpose AsynchronousOperation seen in the latter part of this answer).

    Then you can define a reverse geocode operation:

    class ReverseGeocodeOperation: AsynchronousOperation {
    private static let geocoder = CLGeocoder()
    let location: CLLocation
    private var geocodeCompletionBlock: ((String?) -> Void)?

    init(location: CLLocation, geocodeCompletionBlock: @escaping (String?) -> Void) {
    self.location = location
    self.geocodeCompletionBlock = geocodeCompletionBlock
    }

    override func main() {
    ReverseGeocodeOperation.geocoder.reverseGeocodeLocation(location) { placemarks, _ in
    self.geocodeCompletionBlock?(placemarks?.first?.name)
    self.geocodeCompletionBlock = nil
    self.finish()
    }
    }
    }

    Then you can create a serial operation queue and add your reverse geocode operations to that queue:

    private let geocoderQueue: OperationQueue = {
    let queue = OperationQueue()
    queue.name = Bundle.main.bundleIdentifier! + ".geocoder"
    queue.maxConcurrentOperationCount = 1
    return queue
    }()

    func retrievePlacemarks() {
    for location in locations {
    geocoderQueue.addOperation(ReverseGeocodeOperation(location: location) { string in
    print(string ?? "no name found")
    })
    }
    }
  3. If targeting iOS 13 and later, you can use Combine, e.g. define a publisher for reverse geocoding:

    extension CLGeocoder {
    func reverseGeocodeLocationPublisher(_ location: CLLocation, preferredLocale locale: Locale? = nil) -> AnyPublisher<CLPlacemark, Error> {
    Future<CLPlacemark, Error> { promise in
    self.reverseGeocodeLocation(location, preferredLocale: locale) { placemarks, error in
    guard let placemark = placemarks?.first else {
    return promise(.failure(error ?? CLError(.geocodeFoundNoResult)))
    }
    return promise(.success(placemark))
    }
    }.eraseToAnyPublisher()
    }
    }

    And then you can use a publisher sequence, where you specify maxPublishers of .max(1) to make sure it doesn’t perform them concurrently:

    private var placemarkStream: AnyCancellable?

    func retrievePlacemarks() {
    placemarkStream = Publishers.Sequence(sequence: locations).flatMap(maxPublishers: .max(1)) { location in
    self.geocoder.reverseGeocodeLocationPublisher(location)
    }.sink { completion in
    print("done")
    } receiveValue: { placemark in
    print("placemark:", placemark)
    }
    }

There are admittedly other approaches to make asynchronous tasks run sequentially (often involving calling wait using semaphores or dispatch groups), but I don’t think that those patterns are advisable, so I’ve excluded them from my list of alternatives, above.

How make function of returning address, but not only getting address?

yes, this function always return empty string, coz CLGeocoder().reverseGeocodeLocation(location) take time to get address from Location and at same time your return str! also execute so you get empty string.

use closure to get address from location.

func convertToPlaceMark(_ location: CLLocation, _ handler: @escaping ((String?) -> Void)) {

CLGeocoder().reverseGeocodeLocation(location) {
places,err in

if err != nil {
print("geocoder error")
handler(nil)
return
}
let placeMark1: CLPlacemark? = places!.last
handler(placeMark1?.name)
}
}

Usage

convertToPlaceMark(location) { (address) in
if let address = address {
print(address)
}
}

Trying to use reverseGeocodeLocation, but completionHandler code is not being executing

Your main problem is that your CLGeocoder instance is held in a local variable inside the loop; This means that it will be released before it has completed its task.

You have a couple of other issues too which would cause you problems even if the reverse geo-coding did complete.

The main one is that you are checking for loop termination using a boolean that is set inside the closure; The closure will execute asynchronously, so the loop will have executed many more times before the boolean is set to true in the case where an address is found.

The second problem is related to and made worse by this; reverse geocoding is rate limited. If you submit too many requests too quickly, Apple's servers will simply return an error. Even if you did wait for the first response before submitting a second, your chances of hitting land at random are pretty low, so you will probably hit this limit pretty quickly.

Ignoring the rate limit problem for the moment, you can use a recursive function that accepts a completion handler rather than using a loop and trying to return a value.

var geoCoder = CLGeocoder()

func pinALandPlaceMark(completion: @escaping (Result<MKAnnotation, Error>) -> Void) {
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
geocoder.reverseGeocodeLocation(randomCoordinate) { (placemarks, error) in
guard error == nil else {
completion(nil,error)
return error
}
if let placemark = placemarks.first, let _ = placemark.country {
let randomPinLocation = MKPointAnnotation()
randomPinLocation.coordinate = randomCoordinate.coordinate
completionHandler(randomPinLocation,nil)
} else {
pinALandPlaceMark(completion:completion)
}
}
}

The first thing we do is declare a property to hold the CLGeocoder instance so that it isn't released.

Next, this code checks to see if a placemark with a country was returned. If not then the function calls itself, passing the same completion handler, to try again. If an error occurs then the completion handler is called, passing the error

To use it, you would say something like this:

pinALandPlaceMark() { result in
switch result {
case .success(let placemark):
print("Found \(placemark)")
case .failure(let error):
print("An error occurred: \(error)")
}
}

How to return a value from CLGeocoder?

If I understand your issue correctly you need to update your UI after the geocoding is complete. Like the following:

func updateWeatherData(json: JSON?) {

if let json = json {
weatherData.temperature = fahrenheitToCelcius(json["currently"]["temperature"].doubleValue)
weatherData.weatherIconName = json["currently"]["icon"].stringValue

let location = CLLocation(latitude: json["latitude"].doubleValue, longitude: json["longitude"].doubleValue)
CLGeocoder().reverseGeocodeLocation(location) { (placemark, error) in

if let city = placemark {
self.weatherData.city = city.last?.locality
} else if let error = error {
print(error)
}
self.updateUIWithWeatherData()
}
}

}

The order of these operations is so that the geocoding is done asynchronously and may occur later then the code called after it. Note may but does not need to.

You should also read documentation of this method about threading. UI must be updated on main thread so unless the documentation specifies that the call will be done on main thread you are best forcing it:

CLGeocoder().reverseGeocodeLocation(location) { (placemark, error) in    
if let city = placemark {
self.weatherData.city = city.last?.locality
} else if let error = error {
print(error)
}
DispatchQueue.main.async {
self.updateUIWithWeatherData()
}
}

Set address string with reverseGeocodeLocation: and return from method

Because reverseGeocodeLocation has a completion block, it is handed off to another thread when execution reaches it - but execution on the main thread will still continue onto the next operation, which is NSLog(returnAddress). At this point, returnAddress hasn't been set yet because reverseGeocodeLocation was JUST handed off to the other thread.

When working with completion blocks, you'll have to start thinking about working asynchronously.

Consider leaving reverseGeocodeLocation as the last operation in your method, and then calling a new method with the remainder of the logic inside the completion block. This will ensure that the logic doesn't execute until you have a value for returnAddress.

- (void)someMethodYouCall 
{
NSLog(@"Begin");
__block NSString *returnAddress = @"";

[self.geoCoder reverseGeocodeLocation:self.locManager.location completionHandler:^(NSArray *placemarks, NSError *error) {
if(error){
NSLog(@"%@", [error localizedDescription]);
}

CLPlacemark *placemark = [placemarks lastObject];

startAddressString = [NSString stringWithFormat:@"%@ %@\n%@ %@\n%@\n%@",
placemark.subThoroughfare, placemark.thoroughfare,
placemark.postalCode, placemark.locality,
placemark.administrativeArea,
placemark.country];
returnAddress = startAddressString;

//[self.view setUserInteractionEnabled:YES];

NSLog(returnAddress);
NSLog(@"Einde");

// call a method to execute the rest of the logic
[self remainderOfMethodHereUsingReturnAddress:returnAddress];
}];
// make sure you don't perform any operations after reverseGeocodeLocation.
// this will ensure that nothing else will be executed in this thread, and that the
// sequence of operations now follows through the completion block.
}

- (void)remainderOfMethodHereUsingReturnAddress:(NSString*)returnAddress {
// do things with returnAddress.
}

Or you can use NSNotificationCenter to send a notification when reverseGeocodeLocation is complete. You can subscribe to these notifications anywhere else you need it, and complete the logic from there. Replace [self remainderOfMethodHereWithReturnAddress:returnAddress]; with:

NSDictionary *infoToBeSentInNotification = [NSDictionary dictionaryWithObject:returnAddress forKey:@"returnAddress"];

[[NSNotificationCenter defaultCenter]
postNotificationName:@"NameOfNotificationHere"
object:self
userInfo: infoToBeSentInNotification];
}];

Here's an example of using NSNotificationCenter.

CLGeocoder reverseGeocodeLocation results when no Internet connection is available

That's an iOS 5.0.x behavior. In 5.1 and later, it turns kCLErrorNetwork as you would have expected. If you use the macros from https://stackoverflow.com/a/5337804/1271826 you could theoretically do something like:

[geocoder reverseGeocodeLocation:whatever completionHandler:^(NSArray *placemarks, NSError *error) {

if (error)
{
if (error.code == kCLErrorNetwork || (error.code == kCLErrorGeocodeFoundPartialResult && SYSTEM_VERSION_LESS_THAN(@"5.1")))
{
Alert(@"No Internet connection!");
}
}
else
{
// ...
}
}];

That way, you'll handle the network error regardless of what iOS version the user is running (though, obviously, only 5.0 and later, given that CLGeocoder was introduced in iOS 5.0).



Related Topics



Leave a reply



Submit