Passing in values to a closure function - ios

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.

Related

iOS - How do I wait for the code to finish before going to the next view controller?

I'm very new to coding and I'm stuck on what to do. I'm trying to get the user's geocoordinates from an address, use the coordinates to figure out some values then go to a different view controller where some code will be run to display the values that I figured out. The problem is it finds the users coordinates, Then goes to the next view controller where it doesn't have the calculated data needed to display it then tries to calculate the values needed from the first controller. How do I get this code to run in order?
My Code
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: AdressInput) { coordinate, error in
if error != nil {
// Error
return
} else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: user_lat, savedLong: user_long) // Lastly goes here
}
}
performSegue(withIdentifier: "TimeNavigation", sender: self) // Goes here second
}
The Function
func getCoordinate(addressString: String, completionHandler: #escaping (CLLocationCoordinate2D, NSError?) -> Void ) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
getData Function
func getData(savedLat: String, savedLong: String) {
guard let url = URL(string: "http://127.0.0.1:5000/api/lat/\(savedLat)/long/\(savedLong)") else{return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
var dataAsString = String(data: data, encoding: .utf8)
let splits = dataAsString?.components(separatedBy: "|")
let Counter:Int = splits?.count ?? 0
for n in 0...(Counter-1){
let splits2 = splits?[n].components(separatedBy: ",")
for x in 0...9 {
dataArray[n][x] = String(splits2?[x] ?? "nil")
}
}
}.resume()
}
Write it inside the closure because your performSegue execute before the closure result ... so write it inside the closure but on main thread
Update you getData function
typealias CompletionHandler = (_ success:Bool) -> Void
func getData(savedLat:String,savedLong:String, completionBlock:#escaping CompletionHandler){
guard let url = URL(string: "http://127.0.0.1:5000/api/lat/\(savedLat)/long/\(savedLong)") else{return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completionBlock(false)
return
}
var dataAsString = String(data: data, encoding: .utf8)
let splits = dataAsString?.components(separatedBy: "|")
let Counter:Int = splits?.count ?? 0
for n in 0...(Counter-1){
let splits2 = splits?[n].components(separatedBy: ",")
for x in 0...9 {
dataArray[n][x] = String(splits2?[x] ?? "nil")
}
completionBlock(true)
}
}.resume()
}
And then your BSearch method
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: "AdressInput") { coordinate, error in
if error != nil {
// Error
return
}
else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: "user_lat", savedLong: "user_long", completionBlock: {[weak self] success in
DispatchQueue.main.async {
self?.performSegue(withIdentifier: "TimeNavigation", sender: self)
}
}) // Lastly goes here
}
}
}
You are calling your performSegue outside the scope of getCordinate function, which is why its getting called on click of the button and not waiting for the completion handler to finish.
Just move it inside and it will work fine.
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: AdressInput) { coordinate, error in
if error != nil {
// Error
return
}
else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: user_lat, savedLong: user_long) // Lastly goes here
DispatchQueue.main.async { //when performing UI related task, it should be on main thread
self.performSegue(withIdentifier: "TimeNavigation", sender: self)
}
}
}
}

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)
}
}

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!

Swift: Asynchronous call in a for loop

I am trying to do the following in swift - Trying to reverse decode a list of addresses in an array and print their latitude/longitude coordinates. The code I have is as follows.
let addressArray = ["Address 1", "Address 2"]
var coordinatesArray = [CLLocationCoordinate2D]()
override func viewDidLoad() {
super.viewDidLoad()
createAddressList()
printAddressList()
}
func printAddressList() {
for i in 0 ..< addressArray.count {
print("Address = \(addressArray[i]) Coordinates = \(coordinatesArray[i].latitude),\(coordinatesArray[i].latitude)")
}
func createAddressList() {
for i in 0 ..< addressArray.count {
let address = addressArray[i]
geocoder.geocodeAddressString(address, completionHandler: {(placemarks, error) -> Void in
print("Address = \(address)");
if let placemark = placemarks?.first {
let coordinate = placemark.location?.coordinate
self.coordinatesArray.append(coordinate!)
}
})
}
}
}
The code prints only the first address that's decoded and nothing happens then.
I do have a fix for this like the below one, which is to move the printAddressList call from viewDidLoad method like this
func createAddressList() {
if count < self.addressArray.count {
let address = addressArray[count]
geocoder.geocodeAddressString(address, completionHandler: {(placemarks, error) -> Void in
print("Address = \(address)");
if let placemark = placemarks?.first {
let coordinate = placemark.location?.coordinate
self.coordinatesArray.append(coordinate!)
}
print("Count = \(self.count)")
self.count += 1
self.createAddressList()
})
} else {
printAddressList()
}
}
Even though the latter solution works, I see that it's not clean, would like to know the right way to do this while making the code readable and clean.
How about using this structure?
let workGroup = dispatch_group_create()
for i in 0..<addressArray.count {
dispatch_group_enter(workGroup)
performGeoCoding({ successCallback :
dispatch_group_leave(workGroup)
})
}
dispatch_group_notify(workGroup, dispatch_get_main_queue()){
successCallback()
printAddressList()
}
There is very nice tutorial about dispatch_group here.
A bit more updated would be something like:
let dispatchGroup = DispatchGroup()
for address in addressArray {
dispatchGroup.enter()
performGeoCoding { address in
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
completionHandler()
}

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

Resources