How to fix geocodeAddressString closure in for each - swift

I have task model in database (using realm), which consists of id, title, distance, longitude, latitude, customerAddress. I'm trying update my distance to task. I'm new to swift so I do not understand how should I fix geoCoder.geocodeAddressString closure, so that all tasks would update with their distance. (when task does not have latitude and longitude I check if task has customeradress by using geocodeAddressString
func updateTasksDistance() {
// get tasks for db
guard let tasks = Task.getAllUserTasks() else { return }
// last tracked location
guard let lastLocation = lastLocation else { return }
let myLocation = CLLocation(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)
var distance = 0
tasks.forEach({ (task) in
// check if task has longitude and latitude
if let lat = Double(task.latitude), let long = Double(task.longitude), lat != 0 && long != 0 {
let taskLocation = CLLocation(latitude: lat, longitude: long)
distance = Int(taskLocation.distance(from: myLocation))
} else if !task.customerAddress.isEmpty { // check if task has address
geoCoder.geocodeAddressString(task.customerAddress) { placemarks, _ in
if let placemark = placemarks?.first, let location = placemark.location {
self.taskLocationCoordinate = CLLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude )
}
}
}
// check if we have closure location??
if let taskLocation = taskLocationCoordinate {
distance = Int(CLLocation(latitude: taskLocation.coordinate.latitude, longitude: taskLocation.coordinate.longitude).distance(from: myLocation))
taskLocationCoordinate = nil
}
// update my distance to task
updateTaskDistanceDb(task: task, with: distance)
// reset distance
distance = 0
})
}
// update task distance in db
fileprivate func updateTaskDistanceDb(task: Task, with distance: Int) {
let realm = try? Realm()
if let realm = realm {
do {
try realm.write {
task.distance = distance
}
} catch {
print("error")
}
}
}
Current result: distance gets updated correctly where closure is not called, but when closure is getting called then I get out of order results
expected result: all tasks distance relative to mine updated correctly

Fixed this issue by using this code:
fileprivate func geoCode(addresses: [String], results: [CLPlacemark] = [], completion: #escaping ([CLPlacemark]) -> Void ) {
guard let address = addresses.first else {
completion(results)
return
}
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address) { placemarks, _ in
var updatedResults = results
if let placemark = placemarks?.first {
updatedResults.append(placemark)
}
let remainingAddresses = Array(addresses[1..<addresses.count])
self.geoCode(addresses: remainingAddresses, results: updatedResults, completion: completion)
}
}
func updateTasksDistance() {
// get tasks for db
guard let tasks = Task.getAllUserTasks() else { return }
// last tracked location
guard let lastLocation = lastLocation else { return }
let myLocation = CLLocation(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)
let dispatchGroup = DispatchGroup()
for task in tasks where !task.customerAddress.isEmpty {
let addresses = [task.customerAddress]
dispatchGroup.enter()
geoCode(addresses: addresses) { results in
guard let customerAdress = results.first else { return }
guard let customerLocatin = customerAdress.location else { return }
let taskLocation = CLLocation(latitude: customerLocatin.coordinate.latitude,
longitude: customerLocatin.coordinate.longitude )
// do additional sutff
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
// got all the address
}
})
}
Recursive geocode function helped to calculate all coordinates and dispatchGroup.notify is for waiting till all addresses are geocoded.

Related

SwiftUI async data receive from #escaping closure

I am having trouble with working over data I receive from server.
Each time server sends data it is new coordinates for each user. I am looping over each incoming data, and I want to send the data in completion to receive them on other end. And update model class with them. At the moment I have two users in server. And sometimes the closure passes data two times, but sometimes just one. And interesting thing is that class properties are not updated, at least I dont see them on UI.
This is function I call when data is received. Response is just string I split to get user data.
func updateUserAdrress(response: String, completion: #escaping (Int, Double, Double, String) -> Void){
var data = response.components(separatedBy: "\n")
data.removeLast()
data.forEach { part in
let components = part.components(separatedBy: ",")
let userID = components[0].components(separatedBy: " ")
let id = Int(userID[1])
let latitude = Double(components[1])!
let longitude = Double(components[2])!
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + " " + subThoroughfare
DispatchQueue.main.async {
completion(id!, latitude, longitude, collectedAddress)
}
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
}
In this function I try to invoke the changes on objects. As the incoming data from server is different the first time I have splited the functionality, so the correct block of code would be called.
func collectUsers(_ response: String){
if users.count != 0{
updateUserAdrress(response: response) { id, latitude, longitude, address in
if let index = self.users.firstIndex(where: { $0.id == id }){
let user = self.users[index]
user.latitude = latitude
user.longitude = longitude
user.address = address
}
}
}else{
var userData = response.components(separatedBy: ";")
userData.removeLast()
let users = userData.compactMap { userString -> User? in
let userProperties = userString.components(separatedBy: ",")
var idPart = userProperties[0].components(separatedBy: " ")
if idPart.count == 2{
idPart.removeFirst()
}
guard userProperties.count == 5 else { return nil }
guard let id = Int(idPart[0]),
let latitude = Double(userProperties[3]),
let longitude = Double(userProperties[4]) else { return nil }
let collectedUser = User(id: id, name: userProperties[1], image: userProperties[2], latitude: latitude, longitude: longitude)
return collectedUser
}
DispatchQueue.main.async {
self.users = users
}
}
}
As I also need user address when app starts in model I have made simular function to call in init so it would get address for user. That seems to be working fine. But for more context I will add the model to.
class User: Identifiable {
var id: Int
let name: String
let image: String
var latitude: Double
var longitude: Double
var address: String = ""
init(id: Int, name: String, image: String, latitude: Double, longitude: Double){
self.id = id
self.name = name
self.image = image
self.latitude = latitude
self.longitude = longitude
getAddress(latitude: latitude, longitude: longitude) { address in
self.address = address
}
}
func getAddress(latitude: Double, longitude: Double, completion: #escaping (String) -> Void){
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + " " + subThoroughfare
completion(collectedAddress)
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
}
And one interesting thing. That when closure receives two times data and I assign them to class, there are no changes on UI.
I made this project on Xcode 13.4.1 because on Xcode 14 there is a bug on MapAnnotations throwing purple warnings on view changes.

How to properly wait until function has finished doing in swift?

I have now tried lots of things, but none of them seem to work.
I have a for loop which parses some data and converts coordinates into ZIP string:
for i in 0 ... results.count - 1
{
result = results[i]
self.coordinateToString(lat: result.lat, long: result.long, completion: { (place) in
someCell.label.text = place
})
}
func coordinateToString(lat: Double, long: Double, completion: #escaping (String) -> ()) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: lat, longitude: long)
var ret = ""
geoCoder.reverseGeocodeLocation(location, completionHandler:
{
placemarks, error -> Void in
guard let placeMark = placemarks?.first else { return }
if let zip = placeMark.postalCode, let town = placeMark.subAdministrativeArea
{
let toAppend = "\(zip)" + " \(town)"
ret = toAppend
}
})
DispatchQueue.main.async {
completion(ret)
}
}
However I never manage to show the correct place in the cell, it always shows empty space because it somehow doesn't wait for the completion handler to finish converting. What am I doing wrong here?
This happens because reverseGeocodeLocation returns right away and its completion handler runs afterwards. This means that ret value may be empty when it gets put on the main queue. You should dispatch to main from within the callback, like so:
func coordinateToString(lat: Double, long: Double, completion: #escaping (String) -> ()) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: lat, longitude: long)
var ret = ""
geoCoder.reverseGeocodeLocation(location, completionHandler:
{
placemarks, error -> Void in
guard let placeMark = placemarks?.first else { return }
if let zip = placeMark.postalCode, let town = placeMark.subAdministrativeArea
{
let toAppend = "\(zip)" + " \(town)"
ret = toAppend
DispatchQueue.main.async {
completion(ret)
}
}
})
Of course, given this scenario, you need to handle error cases accordingly. Better yet, use defer, that way completion gets called regardless of what happens:
func coordinateToString(lat: Double, long: Double, completion: #escaping (String) -> ()) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: lat, longitude: long)
var ret = ""
geoCoder.reverseGeocodeLocation(location, completionHandler:
{
defer {
DispatchQueue.main.async {
completion(ret)
}
}
placemarks, error -> Void in
guard let placeMark = placemarks?.first else { return }
if let zip = placeMark.postalCode, let town = placeMark.subAdministrativeArea
{
let toAppend = "\(zip)" + " \(town)"
ret = toAppend
}
})

swift find the 2nd and 3rd lowest numbers in an array -

Below I have a list of cities and I compare a user's current location to the cities and use the (min) function
let closestCity = min(theDistanceInMetersFromBusselton,theDistanceInMetersFromBunbury,theDistanceInMetersFromJoondalup,theDistanceInMetersFromArmadale)
to return the closest city, though now I would like to return the second and third closest city.
I haven't been able to get this to work as yet though I'm thinking something along the lines of:
citiesArray - closestCity = SecondClosestCitiesArray
then do a secondClosestCity = min(SecondClosestCitiesArray) to get the second closest city.
Then repeat this to find the third closest?
Any ideas?
extension HomePage {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let db = Firestore.firestore()
guard let uid = Auth.auth().currentUser?.uid else { return }
let location = locations[0]
let actualLatitude = String(location.coordinate.latitude)
let actualLongitude = String(location.coordinate.longitude)
guard let doubleActualLatitude = Double(actualLatitude) else { return }
guard let doubleActualLongitude = Double(actualLongitude) else { return }
///users current location
let usersCurrentLocation = CLLocation(latitude: doubleActualLatitude, longitude: doubleActualLongitude)
//////////////list of locations ////////////////
//////////ARMADALE/////////////////////
let ArmadaleLatitude = Double(-32.1530)
let ArmadaleLongitude = Double(116.0150)
let ArmadaleCoordinates = CLLocation(latitude:ArmadaleLatitude, longitude: ArmadaleLongitude)
let theDistanceInMetersFromArmadale = usersCurrentLocation.distance(from: ArmadaleCoordinates)
////////////////Bunbury///////////////////
let BunburyLatitude = Double(-33.3256)
let BunburyLongitude = Double(115.6396)
let BunburyCoordinates = CLLocation(latitude:BunburyLatitude, longitude: BunburyLongitude)
let theDistanceInMetersFromBunbury = usersCurrentLocation.distance(from: BunburyCoordinates)
/////////////////////////////////////////////
////////Busselton//////////////////
let busseltonLatitude = Double(-33.6555)
let busseltonLongitude = Double(115.3500)
let busseltonCoordinates = CLLocation(latitude:busseltonLatitude, longitude: busseltonLongitude)
let theDistanceInMetersFromBusselton = usersCurrentLocation.distance(from: busseltonCoordinates)
/////////////////////////////////
/////////Joondalup////////////////////
let JoondalupLatitude = Double(-32.5361)
let JoondalupLongitude = Double(115.7424)
let JoondalupCoordinates = CLLocation(latitude:JoondalupLatitude, longitude: JoondalupLongitude)
let theDistanceInMetersFromJoondalup = usersCurrentLocation.distance(from: JoondalupCoordinates)
//////////////////////////////////////
/////return the the closest city
let closestCity = min(theDistanceInMetersFromBusselton,theDistanceInMetersFromBunbury,theDistanceInMetersFromJoondalup,theDistanceInMetersFromArmadale)
func findClosestCity(){
//////////Armadale////////////////////////
if closestCity == theDistanceInMetersFromArmadale{
db.collection("Users").document(uid).setData(["Location": "Armadale" ], options: SetOptions.merge())
/////////Bunbury////////////
}else if closestCity == theDistanceInMetersFromBunbury{
let Bunbury = "Bunbury"
db.collection("Users").document(uid).setData(["Location": Bunbury ], options: SetOptions.merge())
///////////// Busselton//////////////
}else if closestCity == theDistanceInMetersFromBusselton{
let Busselton = "Busselton"
db.collection("Users").document(uid).setData(["Location": Busselton ], options: SetOptions.merge())
/////////////Joondalup//////////////////
}else if closestCity == theDistanceInMetersFromJoondalup{
db.collection("Users").document(uid).setData(["Location": "Joondalup" ], options: SetOptions.merge())
}
}
}
}
let cityLocations = [
"Armadale": CLLocation(latitude: -32.1530, longitude: 116.0150),
"Bunbury": CLLocation(latitude: -33.3256, longitude: 115.6396),
"Busselton": CLLocation(latitude: -33.6555, longitude: 115.3500),
"Joondalup": CLLocation(latitude: -32.5361, longitude: 115.7424)
]
func distanceFromCity(_ city: String, location: CLLocation) -> Double? {
return cityLocations[city].flatMap { location.distance(from: $0) }
}
func citiesClosestToLocation(_ location: CLLocation, n: Int) -> [String] {
let cities = cityLocations.sorted {
location.distance(from: $0.value) < location.distance(from: $1.value)
}
return cities.dropLast(cities.count - n).map({ $0.key })
}
let testLocation = cityLocations["Armadale"]!
print(citiesClosestToLocation(testLocation, n: 3)) // ["Armadale", "Joondalup", "Bunbury"]
You can add these value into an array and while adding it, make sure that they are getting added to sorting order. The array at any given point will give you closest city on the first index and second closest city on the second index.
Here is an example :
extension Array where Element == Double {
mutating func appendSorted(_ element: Double) {
if self.count == 0 {
self.append(element)
return
}
for i in 0..<self.count {
if element < self[i] {
self.insert(element, at: i)
return
}
}
self.append(element)
}
}

swift 3 Calculate distance to current location and sort result from closet to furthest

I'm trying to to calculate the distance from an event to my current location, sort the results and populate that in a tableview. I keep getting error for optional unwrapped value distance is nil.
private func observeEvents() {
refHandle = ref.observe(.childAdded, with: { (snapshot) -> Void in
let eventDetails = snapshot.value as! Dictionary<String, AnyObject>
let eventID = snapshot.key
let location = eventDetails["location"] as! String!
//calculating distance
self.forwardGeocoding(address: location!)
let distance = self.eventLocation?.distance(from: self.currentLocation!) as Double!
//end calculating
let dateTime = eventDetails["dateTime"] as! String!
let addedByUser = eventDetails["addedByUser"] as! String!
let attending = eventDetails["attendance"] as! String!
if let name = eventDetails["eventName"] as! String! , name.characters.count > 0
{
self.events.append(Events(id:eventID, name: name, location: location!, dateTime: dateTime!, addedByUser: addedByUser!, attending: attending! , distance: distance!))
self.events.sort(by: { $0.distance < $1.distance})
self.tableView.reloadData()
} else {
print("Error ! Can't load events from database")
}
})
} //load events data to uitableview
I created a function to return a CLLocation from an address
func forwardGeocoding(address: String) {
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print(error!)
return
}
if (placemarks?.count)! > 0 {
let placemark = placemarks?[0]
self.eventLocation = placemark?.location
}
})
}
I finally figured out the answer. The issue was the function for distance is called asynchronously there for the result would always be nil. I created a completion handler for the forwardGeocoding function to return latitude and longitude from the address string and call the result inside the nested firebase listener. Here is the code, I hope if someone ran into something similar problem to me will find it helpful.
//Get lat and long
func getCoordinates(address: String, completionHandler: #escaping (_ lat: CLLocationDegrees?, _ long: CLLocationDegrees?, _ error: Error?) -> ()) -> Void {
var _:CLLocationDegrees
var _:CLLocationDegrees
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks: [CLPlacemark]!, error: Error!) in
if error != nil {
print("Geocode failed with error: \(error.localizedDescription)")
} else if placemarks.count > 0 {
let placemark = placemarks[0] as CLPlacemark
let location = placemark.location
let lat = location?.coordinate.latitude
let long = location?.coordinate.longitude
completionHandler(lat, long, nil)
}
}
}
Nested call in firebase listener
refHandle = ref.observe(.childAdded, with: { (snapshot) -> Void in
let location = event["address"] as! String
self.getCoordinates(address: location!) { lat, long, error in
if error != nil {
print("Error")
} else {
self.latitude = lat
self.longitude = long
let distance = CLLocation(latitude: self.latitude!,longitude: self.longitude!).distance(from: self.currentLocation!)
if let name = eventDetails["eventName"] as! String! , name.characters.count > 0
{
self.events.append(Events(id:eventID, name: name, location: location!, dateTime: dateTime!, addedByUser: addedByUser!, attending: attending!, distance: distance))
self.events.sort(by: { $0.distance < $1.distance})
self.tableView.reloadData()
} else {
print("Error ! Can't load events from database")
}
}
}
})

Get Data From Async Completion Handler

Trying to get name of a city, while having latitude and longitude.
Inside a model class Location, I'm using reverseGeocodeLocation(location: , completionHandler: ) func that comes with CLGeocoder (part of CoreLocation).
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
})
}
However, in ViewController, after running the getLocationName(), the location.currentCity is nil, since the completion handler is async, and wasn't finished yet.
How can I make sure that the completion handler is finished running so I can access location.currentCity ?
Pass a closure as a function parameter in your getLocationName which
you can call inside the reverseGeocodeLocation closure.
func updateLocation(currentCity : String) -> Void
{
print(currentCity)
}
func getLocationName(callback : #escaping (String) -> Void)
{
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String
{
self.currentCity = city
callback(city)
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
})
}
In your ViewController...
getLocationName(callback: updateLocation)
I would create a function where location.currentCity is used, and call this function from the completion handler
So if your code looks like:
func foo() {
var location
getLocationName()
print(location.currentcity) // nil
}
change it to:
func foo() {
var location
getLocationName()
}
func bar() {
print(location.currentcity) // someplace
}
and call bar() from your completion handler