iOS Swift - CLGeocoder completionHandler block - ios

I'm trying to parse a location (CLLocation) into a String.
func locationToString (currentLocation: CLLocation) -> String? {
var whatToReturn: String?
CLGeocoder().reverseGeocodeLocation(currentLocation, completionHandler: { (placemarks: [AnyObject]!, error: NSError!) in
if error == nil && placemarks.count > 0 {
let location = placemarks[0] as CLPlacemark
whatToReturn = "\(location.locality) \(location.thoroughfare) \(location.subThoroughfare)"
}
})
return whatToReturn
}
Obviously, whatToReturn always returns null, because completionHandler runs in the background.
I'm having a hard time understanding how do I update my String when completionHandler finishes?
Thanks.

If you want to use your string in a textField, like indicated in your comments, do this:
func getAndDisplayLocationStringForLocation(currentLocation: CLLocation) {
CLGeocoder().reverseGeocodeLocation(currentLocation, completionHandler: { (placemarks: [AnyObject]!, error: NSError!) in
if error == nil && placemarks.count > 0 {
let location = placemarks[0] as CLPlacemark
self.textField.text = "\(location.locality) \(location.thoroughfare) \(location.subThoroughfare)"
}
})
}
However, if you need access elsewhere, perhaps pass a closure as an arg:
func getAndDisplayLocationStringForLocation(currentLocation: CLLocation, withCompletion completion: (string: String?, error?, error: NSError?) -> ()) {
CLGeocoder().reverseGeocodeLocation(currentLocation, completionHandler: { (placemarks: [AnyObject]!, error: NSError!) in
if error == nil && placemarks.count > 0 {
let location = placemarks[0] as CLPlacemark
completion(string: "\(location.locality) \(location.thoroughfare) \(location.subThoroughfare)", error: nil)
} else {
completion(nil, error)
}
})
}
Then call like this:
yourModel.getAndDisplayLocationStringForLocation(someLocation) { (string: String?, error: NSError?) -> () in
if (error == nil) {
self.textField.text = string
}
}
You may want to handle error etc. differently. This should be enough to get you started.

Related

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.

Google Places SDK does not return photos for a popular place. iOS

I use the Google Places API, I get photos for the place, but I ran into such a problem that for the very popular location of Santa Monica Pier, Santa Monica, CA, United States there are no photos. I started to study this question, and what id is returned for this site via Google Places Finder and I returned another id, I tried the id which I returned to the web version for uploading photos and the photos were successfully downloaded.
I got the EjJTYW50YSBNb25pY2EgUGllciwgU2FudGEgTW9uaWNhLCBDQSwgVW5pdGVkIFN0YXRlcw place id with iOS version, it's ChIJm6deTdekwoARTY_RzhoRms0 place id with Google Places Finder.
import GooglePlaces
class GooglePlacesManager {
private let gmsPlacesClient = GMSPlacesClient()
func loadPhotosForPlace(_ placeID: String, success: ((_ photos: [UIImage]) -> Void)?, fail: ((_ error: Error) -> Void)?) {
gmsPlacesClient.lookUpPhotos(forPlaceID: placeID) { [weak self] (photos, error) -> Void in
guard error == nil else {
debugPrint("loadPhotosForPlace", error!.localizedDescription)
fail?(error!)
return
}
guard let results = photos?.results else {
success?([])
return
}
var resultCount = results.count
debugPrint("first result count", resultCount)
if resultCount == 0 {
success?([])
return
}
var photos = [UIImage]()
guard self != nil else { return }
for result in results {
self!.loadImageForMetadata(photoMetadata: result, success: { (photo) in
photos.append(photo)
if resultCount == photos.count {
success?(photos)
}
}, notExist: {
resultCount -= 1
if resultCount == photos.count {
success?(photos)
}
}, fail: { (error) in
resultCount -= 1
if resultCount == photos.count {
success?(photos)
}
})
}
}
}
private func loadImageForMetadata(photoMetadata: GMSPlacePhotoMetadata, success: ((_ photo: UIImage) -> Void)?, notExist: (() -> Void)?, fail: ((_ error: Error) -> Void)?) {
gmsPlacesClient.loadPlacePhoto(photoMetadata, callback: {
(photo, error) -> Void in
guard error == nil else {
debugPrint("loadPhotosForPlace", error!.localizedDescription)
fail?(error!)
return
}
guard photo != nil else {
notExist?()
return
}
success?(photo!)
})
}
func getLocationInfo(_ placeID: String, success: ((_ place: GMSPlace) -> Void)?, fail: ((_ error: Error) -> Void)?) {
gmsPlacesClient.lookUpPlaceID(placeID) { (place, error) in
guard error == nil else {
debugPrint(error!.localizedDescription)
fail?(error!)
return
}
guard place != nil else { return }
success?(place!)
}
}
}
Please tell me how it can be fixed?
Update:
I added these lines of code in this loadPhotosForPlace functions
gmsPlacesClient.lookUpPlaceID(placeID, callback: { (place, error) -> Void in
if let error = error {
print("lookup place id query error: \(error.localizedDescription)")
return
}
guard let place = place else {
print("No place details for \(placeID)")
return
}
print("Place name \(place.name)")
print("Place address \(place.formattedAddress)")
print("Place placeID \(place.placeID)")
print("Place attributions \(place.attributions)")
})
I got this logs but a I didn't get any photos.
Place name Santa Monica Pier
Place address Optional("Santa Monica Pier, Santa Monica, CA 90401, USA")
Place placeID EjJTYW50YSBNb25pY2EgUGllciwgU2FudGEgTW9uaWNhLCBDQSwgVW5pdGVkIFN0YXRlcw
Place attributions nil

CLGeocoder error. GEOErrorDomain Code=-3

When I tried to use reverse geocoding,this error message showed up.
Geocode error: Error Domain=GEOErrorDomain Code=-3 "(null)"
My code is below:
import CoreLocation
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if let placemarks = placemarks {
reverseGeocodeLocations[hash] = placemarks
}
callback(placemarks, error)
}
This works only time to time, and I request reverseGeocode several times per seconds. So I guess this error message is related to the limit of request or something?
Is there any documentation about appleā€™s geocode request?
Thanks for advance.
Updated
here is my entire code for requesting
import CoreLocation
fileprivate struct ReverseGeocodeRequest {
private static let geocoder = CLGeocoder()
private static var reverseGeocodeLocations = [Int: [CLPlacemark]]()
private static let reverseGeocodeQueue = DispatchQueue(label: "ReverseGeocodeRequest.reverseGeocodeQueue")
private static var nextPriority: UInt = 0
fileprivate static func request(location: CLLocation, callback: #escaping ([CLPlacemark]?, Error?)->Void) {
let hash = location.hash
if let value = reverseGeocodeLocations[hash] {
callback(value, nil)
} else {
reverseGeocodeQueue.async {
guard let value = reverseGeocodeLocations[hash] else {
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if let placemarks = placemarks {
reverseGeocodeLocations[hash] = placemarks
}
callback(placemarks, error)
}
return
}
callback(value, nil)
}
}
}
let priority: UInt
let location: CLLocation
let handler : ([CLPlacemark]?, Error?)->Void
private init (location: CLLocation, handler: #escaping ([CLPlacemark]?, Error?)->Void) {
ReverseGeocodeRequest.nextPriority += 1
self.priority = ReverseGeocodeRequest.nextPriority
self.location = location
self.handler = handler
}
}
extension ReverseGeocodeRequest: Comparable {
static fileprivate func < (lhs: ReverseGeocodeRequest, rhs: ReverseGeocodeRequest) -> Bool {
return lhs.priority < rhs.priority
}
static fileprivate func == (lhs: ReverseGeocodeRequest, rhs: ReverseGeocodeRequest) -> Bool {
return lhs.priority == rhs.priority
}
}
extension CLLocation {
func reverseGeocodeLocation(callback: #escaping ([CLPlacemark]?, Error?)->Void) {
ReverseGeocodeRequest.request(location: self, callback: callback)
}
func getPlaceName(callback: #escaping (Error?, String?)->Void) {
self.reverseGeocodeLocation { (placemarks, error) in
guard let placemarks = placemarks, error == nil else {
callback(error, nil)
return
}
guard let placemark = placemarks.first else {
callback(nil, "Mysterious place")
return
}
if let areaOfInterest = placemark.areasOfInterest?.first {
callback(nil, areaOfInterest)
} else if let locality = placemark.locality {
callback(nil, locality)
} else {
callback(nil, "On the Earth")
}
}
}
}
After searching everywhere for the answer it was in Apples docs! :/
https://developer.apple.com/reference/corelocation/clgeocoder/1423621-reversegeocodelocation
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 network to your completion handler.
When checking the error code in the completion handler it is indeed Network Error:2
Hope this helps someone!

Callbacks, completion handler with multiple parameters in Swift

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

Setting a property to a value within a closure

I made a property in my view controller:
var cityPlaceMark = CLPlacemark()
And I want to assign it to a placemark in my locationManger didUpdate location function:
CLGeocoder().reverseGeocodeLocation(manager.location, completionHandler:{(placemarks, error)->Void in
if (error != nil) {
println("Reverse geocode failed with error")
return
}
if placemarks.count > 0 {
//var pm = placemarks[0] as CLPlacemark
self.cityPlaceMark = placemarks[0] as CLPlacemark
}
})
The app crashes with a Thread1: EXC_BAD_ACCESS.
If I save it in a variable declared in the closure (shown as "var pm" commented out) it works fine.
Can someone explain to me what I am doing wrong, and how to properly assign it to a property.
Thanks!
It looks like it may be a bug.
If I make it into an optional with:
var cityPlaceMark: CLPlacemark?
and implement it in the closure:
CLGeocoder().reverseGeocodeLocation(manager.location, completionHandler: {(placemarks, error)->Void in
if (error != nil) {
println("Reverse geocode failed with error")
return
}
if placemarks.count > 0 {
self.cityPlaceMark = placemarks[0] as? CLPlacemark
}
})
It now works!

Resources