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

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

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.

Firebase data and unexpected nil

My ultimate goal is to display annotations on a Map from MapKit. However, loading data from firebase does not work.
Here is the code:
var hotels = [Hotel]()
override func viewDidLoad() {
super.viewDidLoad()
map.delegate = self
checkLocationServices()
observe()
}
The function observe is the following:
func observe() {
Data().getAllHotel { (hotelA) -> (Void) in
if let hotel = hotelA {
print("hotel: ", hotel)
self.hotels.append(hotel)
let hotelAnnotation = MKPointAnnotation()
hotelAnnotation.title = "\(hotel.hotelName)"
hotelAnnotation.coordinate = CLLocationCoordinate2D(latitude: hotel.latitude, longitude: hotel.longitude)
self.map.addAnnotation(hotelAnnotation)
}
}
}
Note that the print in the if statement is not empty.
However the line hotelAnnotation.title causes a bug unexpected nil and I don't understand why.
Additionnally here is the getAllHotel function inside the Data class:
func getAllHotel(closure: HotelClosure?) {
let ref = Ref().databaseHotels
ref.observe(.childAdded) { (snapshot) in
if let dict = snapshot.value as? Dictionary<String, AnyObject> {
let hotel = self.fromDictToHotel(key: snapshot.key, dict: dict)
closure?(hotel)
} else {
closure?(nil)
}
}
}
func fromDictToHotel(key: String, dict: Dictionary<String, AnyObject>) -> Hotel? {
guard let hotelName = dict["name"] as? String else { return nil }
guard let latitude = dict["latitude"] as? Double else { return nil }
guard let longitude = dict["longitude"] as? Double else { return nil }
let hotelDescription = dict["description"] as? String
let hotelGrade = dict["grade"] as? Double
let hotelImageUrl = dict["hotelImageUrl"] as? String
let hotel = Hotel(hid: key, hotelName: hotelName, hotelDescription: hotelDescription, latitude: latitude, longitude: longitude, grade: hotelGrade, url: hotelImageUrl)
return hotel
}
Thanks in advance for your help!

How to fix geocodeAddressString closure in for each

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.

Distance from Current Location to Annotation. (Firebase)

I want to have a the distance from my Currentlocation to a annotation that's in the FireData Base. I tried to make it word but i can't ;(. I would like to have the distance between the two locations in a var. I hope you guys can help me.
func reload(){
//get data
Database.database().reference().child("Rollerbanken").observe(.value, with: { (snapshot) in
for item in snapshot.children{
if let value = snapshot.value as? Dictionary<String, Any> {
for key in value.keys {
if let itemDict = value[key] as? Dictionary<String, AnyObject> {
let annotation = MKPointAnnotation()
annotation.title = itemDict["TypeControle"] as! String
let tijd = itemDict["Tijd"] as! String
annotation.subtitle = "Geplaatst om \(tijd)"
let getLatitude = itemDict["Latitude"] as? String
let getLongitude = itemDict["Longitude"] as? String
if let lat = getLatitude, let long = getLongitude {
annotation.coordinate = CLLocationCoordinate2D(latitude: Double(lat)!, longitude: Double(long)!)
self.map.addAnnotation(annotation)
let directionRequest = MKDirectionsRequest()
directionRequest.source = MKMapItem.forCurrentLocation()
if #available(iOS 10.0, *) {
directionRequest.destination = MKMapItem(placemark: MKPlacemark.init(coordinate: CLLocationCoordinate2DMake(Double(lat)!, Double(long)!)))
} else {
// Fallback on earlier versions
}
directionRequest.transportType = .walking
let direction = MKDirections(request: directionRequest)
direction.calculate(completionHandler: { (response, error) in
if error != nil {
print("Error while build route")
} else {
let route = response?.routes.last
let distance = route?.distance
print(distance)
}
})
}
}
}
}
}
})
}
Here is my Structure:
Try to use this code. Don't forget to enable your current location on map
let directionRequest = MKDirectionsRequest()
directionRequest.source = MKMapItem.forCurrentLocation()
directionRequest.destination = MKMapItem(placemark: MKPlacemark.init(coordinate: CLLocationCoordinate2DMake(YOURPOINTLATITUDE, YOURPOINTLONGITUDE)))
directionRequest.transportType = .walking
let direction = MKDirections(request: directionRequest)
direction.calculate(completionHandler: { (response, error) in
if error != nil {
print("Error while build route")
} else {
let route = response?.routes.last
let distance = route?.distance
I have used similar function, NOTE this was my function therefore it has rider and driver.. however you can change it to use annotation and location from firebase.
if let rideRequestDictionary = snapshot.value as? [String:AnyObject] {
// Getting the rider location and email
if let email = rideRequestDictionary["email"] as? String {
if let lat = rideRequestDictionary["lat"] as? Double{
if let lon = rideRequestDictionary["lon"] as? Double{
// Getting the Driver location and email
let driverCLLocation = CLLocation(latitude: driverLocation.latitude, longitude: driverLocation.longitude)
let riderCLLocation = CLLocation(latitude: lat, longitude: lon)
// getting the distance between the two people
let distance = driverCLLocation.distance(from: riderCLLocation) / 1000
// rounding the distances
let roundedDistance = round(distance * 100) / 100
// putting the rounded distance and email in label
cell.textLabel?.text = "\(email) - \(roundedDistance)km away"
}
}
}

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