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.
Related
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()
}
}
I'm trying to delay a segue until I get a response from a reverseGeocodeLocation call. However, when using breakpoints to check when the value actually changes, it's still happening after the UI transition occurs. I've tried having the function be a void and with the current String return.
EDITED CODE:
func getReversedGeocodeLocation(completionHandler: (String, NSError?) ->()) {
let semaphore = dispatch_semaphore_create(0)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
CLGeocoder().reverseGeocodeLocation(self.newMeetupLocation, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
else if placemarks?.count > 0 {
}
else {
print("Problem with the data received from geocoder")
}
completionHandler(placemarks!.first!.name! ?? "", error)
})
dispatch_semaphore_signal(semaphore)
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
OLD CODE:
let semaphore = dispatch_semaphore_create(1) //even with 0, it's not working
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
self.newAddress = self.getReversedGeocodeLocation()
dispatch_semaphore_signal(semaphore)
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
//dispatch_semaphore_signal(semaphore)
print(self.newAddress + ".")
self.performSegueWithIdentifier("mainToNewAddress", sender: self)
func getReversedGeocodeLocation() -> String{
var address = ""
CLGeocoder().reverseGeocodeLocation(self.newAddressLocation, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
else if placemarks?.count > 0 {
let pm = placemarks?.first!
address = pm!.name!
}
else {
print("Problem with the data received from geocoder")
}
})
return address
}
Using a semaphore and dispatching the call to getReversedGeocodeLocation is unnecessarily complicated. CLGeocoder().reverseGeocodeLocation is already asynchronous. If you simply pass a completion handler closure to getReversedGeocodeLocation then you can use that to invoke the segue;
self.getReversedGeocodeLocation(self.newAddressLocation, completionHandler: { (address,error) in
guard error == nil else {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
guard let address = address else {
print("No address returned")
return
}
self.newAddress = address
dispatch_async(dispatch_get_main_queue(), {
self.performSegueWithIdentifier("mainToNewAddress", sender: self)
})
})
func getReversedGeocodeLocation(addressLocation: String, completionHandler:((address: String?, error: NSError?) -> Void))) {
CLGeocoder().reverseGeocodeLocation(self.newAddressLocation, completionHandler: {(placemarks, error) -> Void in
var address = nil
if placeMarks.count > 0 {
if let pm = placeMarks!.first {
address = pm.name
}
} else {
print("Problem with the data received from geocoder")
}
completionHandler(address,error)
})
}
I dont think its a problem with the semaphore, its with your getReversedGeocodeLocation function. the CLGeocoder().reverseGeocodeLocation is an async function, therefore the return statement is going to execute before the completion block of CLGeocoder().reverseGeocodeLocation is executed, which is why self.newAddress is being assigned an empty string.
To fix this you could use another semaphore inside the getReversedGeocodeLocation to block until the completion handler is finished, or use a delegate to notify something that the completion handler is finished (which is usually the right way to do it).
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)
}
}
}
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.
I reverse geocode in a few places in my map and I wanted to wrap it to some common method. What I have is this as a starting point that works:
func reverseGeocodeLocation(location: CLLocation, completion: (CLPlacemark) -> Void) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) {
(placemarks, error) -> Void in
// Check for returned placemarks
if let placemarks = placemarks where placemarks.count > 0 {
let topResult = placemarks[0] as! CLPlacemark
completion(topResult)
}
}
}
However, if I want to add another parameter to my completion method that would return the error to my view like so:
func reverseGeocodeLocation(location: CLLocation, completion: (CLPlacemark, NSError) -> Void) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) {
(placemarks, error) -> Void in
// Check for returned placemarks
if let placemarks = placemarks where placemarks.count > 0 {
let topResult = placemarks[0] as! CLPlacemark
completion(topResult, error)
}
}
}
If error is nil, then my completion(topResult, error) will fail because error is nil with bad access. But I cannot do this since the parameters are not matching the completion handler.
if error != nil {
completion(topResult)
} else {
completion(...) // do something with the error
}
So if I have a nil error, how would I call my completion handler?
Since you own definition of your completion header your can define it normally with optional NSError? So it will not cause errors and will require unwrapping. Moreover recent definition of CLGeocodeCompletionHandler also uses optional array and error:
typealias CLGeocodeCompletionHandler = ([CLPlacemark]?, NSError?) -> Void