swift - How to perform task completion - ios

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

Related

How to get latitude and longitude from Model into my url link

I am trying to get city weather in current location. I have model, where is getting weather data from JSON, and also I have model, where I am getting my location (latitude and longitude). But I don't know, how I can get this latitude and longitude in my link.
WeatherModel: where I use url link. I want use latitude and longitude from my LocationModel below
import Foundation
import CoreLocation
protocol IWeatherService {
func getCitiesWeather(forCoordinates coordinates: CLLocationCoordinate2D, completion: #escaping (Result<CitiesWeather, Error>) -> Void)
}
enum WeatherServiceError: Error {
case badUrl
}
final class WeatherService: IWeatherService {
func weatherURLString(forCoordinates coordinates: CLLocationCoordinate2D) -> String {
return "https://api.openweathermap.org/data/2.5/weather?lat=\(coordinates.latitude)&lon=\(coordinates.longitude)&units=metric&appid=b382e4a70dfb690b16b9381daac545ac&lang=ru"
}
func getCitiesWeather(forCoordinates coordinates: CLLocationCoordinate2D, completion: #escaping (Result<CitiesWeather, Error>) -> Void) {
//weatherURLString(forCoordinates: CLLocationCoordinate2D) ???
//LocationManager.shared.locationManager(CLLocationManager, didUpdateLocations: [CLLocation]) ???
//Проверка, что у нас есть url адрес
guard let url = URL(string: .url) else {
return completion(.failure(WeatherServiceError.badUrl))
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
do {
let result = try JSONDecoder().decode(CitiesWeather.self, from: data)
completion(.success(result))
}
catch {
print("failed to convert \(error)")
}
}
task.resume()
}
}
LocationModel:
import Foundation
import CoreLocation
protocol ILocationService {
func getUserLocation(completion: #escaping ((CLLocation) -> Void))
}
class LocationManager: NSObject, ILocationService, CLLocationManagerDelegate {
static let shared = LocationManager()
let manager = CLLocationManager()
public func getUserLocation(completion: #escaping ((CLLocation) -> Void)) {
self.completion = completion
manager.requestWhenInUseAuthorization()
manager.delegate = self
manager.startUpdatingLocation()
}
public func resolveLocationName(with location: CLLocation, completion: #escaping ((String?) -> Void)) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location, preferredLocale: .current) { placemarks, error in
guard let place = placemarks?.first, error == nil else {
completion(nil)
return
}
print(location)
var name = ""
if let locality = place.locality {
name += locality
}
if let adminRegion = place.administrativeArea {
name += ", \(adminRegion)"
}
completion(name)
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
completion?(location)
//Эти значения надо передать в WeatherService
print(location.coordinate.latitude)
print(location.coordinate.longitude)
manager.stopUpdatingLocation()
}
var completion: ((CLLocation) -> Void)?
}
As vadian said in his comment, this bit can't work:
private extension String {
static let url = "https://api.openweathermap.org/data/2.5/weather?lat=\(coordinates.latitude)&lon=\(coordinates.longitude)&units=metric&appid=b382e4a70dfb690b16b9381daac545ac&lang=ru"
}
You should create a function that takes coordinates and returns a URL string for a weather update:
func weatherURLString(forCoordinates coordinates: CLLocationCoordinate2D) {
return "https://api.openweathermap.org/data/2.5/weather?lat=\(coordinates.latitude)&lon=\(coordinates.longitude)&units=metric&appid=b382e4a70dfb690b16b9381daac545ac&lang=ru"
Rewrite your getCitiesWeather() function to take a parameter coordinates of type CLLocationCoordinate2D, and have it call the above function to generate the URL string it will use.
You have code that asks to update the user's location, and implements the locationManager(_:didUpdateLocations:) function. In your locationManager(_:didUpdateLocations:) function, you call a completion handler. Have that completion handler call the weather service with the user's updated location.

Use variables inside a completion handler somewhere else

I'm trying to convert address to coordinates in order to create a Firestorm GeoPoint object.
I currently have this code:
func getCoords(from address: String, locationCompletionHandler: #escaping (CLLocationCoordinate2D?, Error?) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address) { (placemarks, error) in
guard
let placemarks = placemarks,
let coordinate = placemarks.first?.location?.coordinate
else {
locationCompletionHandler(nil, error)
return
}
locationCompletionHandler(coordinate, nil)
}
}
func addressToGeoPoint(from address: String) -> GeoPoint {
var latitude:Double = 0
var longitude:Double = 0
getCoords(from: address) { coordinate, error in
if let coordinate = coordinate {
latitude = coordinate.latitude
longitude = coordinate.longitude
}
else {
print("Can't get coords: \(String(describing: error?.localizedDescription))")
}
}
return GeoPoint(latitude: latitude, longitude: longitude)
}
The problem is that when the GeoPoint object is being initialized, the latitude and longitude variables are still 0 because the completion handler hasn't finished yet.
The function addressToGeoPoint must return a GeoPoint.
What can I do in order for this to work?
Thanks!

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.

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

Resources