Completion handler asynchrony - ios

I have a problem with my completion handlers. Here is a function with a completion handler, located in a Utility file:
func convertGeopointToCity(geopoint: PFGeoPoint, complete: (city: String, error: NSError?) -> Void) {
var city = ""
let latitude = geopoint.latitude
let longitude = geopoint.longitude
let location: CLLocation = CLLocation(latitude: latitude, longitude: longitude)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: { placemarks, error in
if (error == nil) {
if let p = CLPlacemark?(placemarks![0]) {
if let city = p.locality {
city = " \(city)!"
print("Here's the city:\(city)")
complete(city: city, error: nil)
}
}
}
})
}
That I call in a ViewController
LocationUtility.instance.convertGeopointToCity(geopoint, complete: { result, error in
if error != nil {
print("error converting geopoint")
} else {
city = result as String
}
})
print("The city: \(city)")
The output clearly indicates that the function is not waiting to be completed before running the block:
The city:
Here's the hood Toronto!
How do I address this issue?

You should put your handler inside of the block:
LocationUtility.instance.convertGeopointToCity(geopoint, complete: { result, error in
if error != nil {
print("error converting geopoint")
} else {
city = result as String
// do other stuff here, or call a method
print("The city: \(city)")
}
})
The CLGeocoder().reverseGeocodeLocation is async. The completion block is called as soon as the geocode finished, more likely after your print append.
You should do your operations in another function called by the complete block, or adding some code to the block itself.

Related

How to return a value from CLGeocoder?

I'm working on the weather app as a training and there's a need to convert location to city. So I'm using CLGeocoder like so:
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)
}
}
updateUIWithWeatherData()
}
}
//MARK: - UI Updates
func updateUIWithWeatherData() {
cityLabel.text = weatherData.city
temperatureLabel.text = "\(weatherData.temperature)°"
weatherIcon.image = UIImage(named: weatherData.weatherIconName!)
}
And this code returns nil to weatherData.city. But when I place a breakpoint inside a closure, everything works fine. What am I missing?
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()
}
}

swift - How to perform task completion

I'm trying to do reverse geocoding for multiple locations at the same time. So I create a function performReverseGeoLocation. The problem is, that since CLGeocoder().reverseGeocodeLocation a closure, the completionHandlerLocations will get executed first. How do I change these functions so that the caller will get completion handler after all CLGeocoder().reverseGeocodeLocation inside the for loop is done?
Code I have tried:
private func getImageLocation() {
performReverseGeoLocation(completionHandlerLocations: { (cities, countries) in
print("***** This is executed before the reverse geo code location is done")
})
}
private func performReverseGeoLocation(completionHandlerLocations: #escaping (_ cities: [String], _ countries: [String]) -> Void) {
var cities = [String]()
var countries = [String]()
for image in self.images {
let longitude = image.longitude
let latitude = image.latitude
let location = CLLocation(latitude: latitude, longitude: longitude)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print("***** This is executed after completionHandlerLocations is done")
if error != nil {
self.alertError("Reverse geocoder failed with error" + (error?.localizedDescription)!)
return
}
if placemarks!.count > 0 {
let pm = placemarks![0]
let country = pm.country
let city = pm.locality
if (!cities.contains(city!)) {
cities.append(city!)
}
if (!countries.contains(country!)) {
countries.append(country!)
}
}
else {
self.alertError("Fail to perform reverse geo location")
}
})
}
// THIS IS WILL EXECUTED FIRST
completionHandlerLocations(cities, countries)
}
You can do something like this:
var count = 0
for image in self.images {
...
CLGeocoder().reverseGeocodeLocation(location) {
// get result
counter ++
if count == self.images.count { // finish all requests
completionHandlerLocations(cities, countries)
}
}
}
That's the most simple way to do.
As i-am-jorf mentioned, you can create a DispatchGroup and wait for the notification when all reverse geocoding tasks are complete:
private func performReverseGeoLocation(completionHandlerLocations: #escaping (_ cities: [String], _ countries: [String]) -> Void) {
let group = DispatchGroup()
var cities = [String]()
var countries = [String]()
self.images.forEach { (location) in
group.enter()
let longitude = image.longitude
let latitude = image.latitude
let location = CLLocation(latitude: latitude, longitude: longitude)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (placemark, error) in
// do all your checks...
if placemark != nil && placemark!.count > 0 {
cities.append(placemark!.first!.locality!)
countries.append(placemark!.first!.country!)
}
group.leave()
})
}
group.notify(queue: DispatchQueue.main) {
completionHandlerLocations(cities, countries)
}
}

Passing in values to a closure function

Check out this code:
func getReversedGeocodeLocation(location: CLLocation, completionHandler: #escaping ()->()) {
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks != nil {
if placemarks!.count > 0 {
let pm = placemarks![0]
if let addressDictionary: [AnyHashable: Any] = pm.addressDictionary,
let addressDictionaryFormatted = addressDictionary["FormattedAddressLines"] {
let address = (addressDictionaryFormatted as AnyObject).componentsJoined(by: ", ")
self.addressInViewController = address
}
completionHandler()
}
} else {
print("Problem with the data received from geocoder")
}
})
}
In the viewController
override func viewDidLoad() {
var addressInViewController = String()
getReversedGeocodeLocation(location: location, completionHandler: {
print("After geo finished")
})
}
This is a simple case for using closures. As you can see, when the reverse geo finishes, it updates the addressInViewController variable which is defined outside the function itself. I'm a bit confused when it comes to closures but I do know it's essentially passing in another function as a parameter, into a function. So can I pass in something like (_ String: x)->() instead of ()->() where the address variable would be populated from the main reverse geo function and passed along? I tried doing that but it says "x" is undefined. If this is achieve-able then I guess I can decouple my code in a better way using closures.
Thanks and have a great day :)
define your methods like this
func getReversedGeocodeLocation(location: CLLocation, completionHandler: #escaping (_ value : Any)->()) {
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks != nil {
if placemarks!.count > 0 {
let pm = placemarks![0]
if let addressDictionary: [AnyHashable: Any] = pm.addressDictionary,
let addressDictionaryFormatted = addressDictionary["FormattedAddressLines"] {
let address = (addressDictionaryFormatted as AnyObject).componentsJoined(by: ", ")
self.addressInViewController = address
}
completionHandler(address)
}
} else {
print("Problem with the data received from geocoder")
}
})
}
override func viewDidLoad() {
var addressInViewController = String()
getReversedGeocodeLocation(location: location, completionHandler: { (_ values : Any) in
self. addressInViewController = values
})
}
Make dataType of Value according to your need.

GCD Semaphore does not wait (Swift)

I'm pretty much new to GCD. I have this function for forward geocoding and the issue is it returns before the completion closure completes. So every time it just returns nil. I found out I can use semaphores so the return waits for the completion closure to complete, but there are very little examples online and I found none of a function that returns. I tried to implement it, but function still returns nil even though the location is printed out to the console moments later. If someone could tell me where I am making a mistake I would be very grateful.
func forwardGeocoding(address: String) -> CLLocation? {
var userLocation: CLLocation?
var returnvalue: CLLocation?
let semaphore = dispatch_semaphore_create(0)
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print("Geocoding error: \(error)")
return
}
if placemarks?.count > 0 {
let placemark = placemarks?.first
let location = placemark?.location
let coordinate = location?.coordinate
print("Settings location: \(coordinate!.latitude), \(coordinate!.longitude)")
if let unwrappedCoordinate = coordinate {
let CLReadyLocation: CLLocation = CLLocation(latitude: unwrappedCoordinate.latitude, longitude: unwrappedCoordinate.longitude)
userLocation = CLReadyLocation
dispatch_semaphore_signal(semaphore)
}
}
})
let wait = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
if wait != 0 {
returnvalue = userLocation
}
return returnvalue
}
As Paulw11 already mentioned, a semaphore in this case is very bad programming habit. If you are new to GCD learn to understand the asynchronous pattern for returning received data in a completion block. It's even simpler to handle in Swift than in Objective-C.
This is an example using a completion block:
func forwardGeocoding(address: String, completion: (CLLocation?, NSError?) -> Void) {
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
completion(nil, error!)
} else {
if let placemarks = placemarks where !placemarks.isEmpty {
let placemark = placemarks.first!
if let unwrappedLocation = placemark.location {
let coordinate = unwrappedLocation.coordinate
print("Settings location: \(coordinate.latitude), \(coordinate.longitude)")
let CLReadyLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
completion(CLReadyLocation, nil)
}
}
}
})
}
and call it with
forwardGeocoding("foo") { (location, error) in
if error != nil {
print("Geocoding error: \(error!)")
} else {
// do something with the location
}
}
the result of dispatch_semaphore_wait is 0 on success and non zero on timeout occurred. So you should change your code to:
let wait = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
if wait == 0 { //your work is done, without time out.
returnvalue = userLocation //update location
}
return returnvalue //otherwise return nil according to your code above. this code will never execute. In this case, there is no time out cause it wait forever.
Another point, you must call dispatch_semaphore_signal before your block geocodeAddressString end the execution. Otherwise, your application will wait forever in case of getting error.
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print("Geocoding error: \(error)")
dispatch_semaphore_signal(semaphore) //add to here
return
}
if placemarks?.count > 0 {
let placemark = placemarks?.first
let location = placemark?.location
let coordinate = location?.coordinate
print("Settings location: \(coordinate!.latitude), \(coordinate!.longitude)")
if let unwrappedCoordinate = coordinate {
let CLReadyLocation: CLLocation = CLLocation(latitude: unwrappedCoordinate.latitude, longitude: unwrappedCoordinate.longitude)
userLocation = CLReadyLocation
}
}
dispatch_semaphore_signal(semaphore) //and here
})
Finally, when using semaphore that wait forever, you have to make sure that the block of code inside semaphore will end the execution.

Why .geocodeAddressString doesn't change external variable?

I have an external variable chooseCoordinates, which has a type of CLLocationCoordinate2D. This var need to save coordinates from geocodeAddressString closure, but, apparently, it doesn't change.
I would like to ask for advice, how to make this actually closure store the data, so I will be able to parse it to another viewController
var chooseCoordinates = CLLocationCoordinate2D()
////////////
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(sender.text!, completionHandler: { (placemarks, error) -> Void in
if(error != nil) {
print("\(error)")
}
if let placemark = placemarks?.first {
let coordinates: CLLocationCoordinate2D = placemark.location!.coordinate
self.chooseCoordinates = coordinates
}
})
The geocodeAddressString runs asynchronously (i.e. even though the method returns immediately, its completionHandler closure may be called later). So, are you sure it's not changing, or just not changing by the time you try to use chooseCoordinates? You should initiate whatever updating of the UI or whatever from within the closure (or the didSet of chooseCoordinates), not doing so immediately. We don't see how you're using chooseCoordinates, so it's hard to be more specific than that.
For example:
geocoder.geocodeAddressString(sender.text!) { placemarks, error in
if error != nil {
print(error!)
}
if let placemark = placemarks?.first {
let coordinates: CLLocationCoordinate2D = placemark.location!.coordinate
self.chooseCoordinates = coordinates
// call the method that uses `chooseCoordinates` here
}
}
// don't try to use `chooseCoordinates` here, as it hasn't been set yet.
Or, you might employ the completionHandler pattern yourself:
#IBAction func didEndEditingTextField(sender: UITextField) {
geocodeAddressString(sender.text!) { coordinate, error in
self.chooseCoordinates = coordinate
// trigger whatever the next step is here, or in the `didSet` of `chooseCoordinates`
}
// don't try to use `chooseCooordinates` here
}
var chooseCoordinates: CLLocationCoordinate2D? // you probably should make this optional
let geocoder = CLGeocoder()
/// Geocode string
///
/// - parameter string: The string to geocode.
/// - parameter completionHandler: The closure that is called asynchronously (i.e. later) when the geocoding is done.
func geocodeAddressString(string: String, completionHandler: (CLLocationCoordinate2D?, NSError?) -> ()) {
geocoder.geocodeAddressString(string) { placemarks, error in
if error != nil {
print(error!)
}
if let placemark = placemarks?.first {
completionHandler(placemark.location!.coordinate, error)
} else {
completionHandler(nil, error)
}
}
}

Resources