Swift Firebase Sort By Distance - ios

I am trying to sort my array by distance. I already have everything hooked up to grab the distance's but unsure how to sort from closest to furthest from the users location. I've used the below code for MKMapItem's yet unsure how to apply to my current array.
func sortMapItems() {
self.mapItems = self.mapItems.sorted(by: { (b, a) -> Bool in
return self.userLocation.location!.distance(from: a.placemark.location!) > self.userLocation.location!.distance(from: b.placemark.location!)
})
}
Firebase Call
databaseRef.child("Businesses").queryOrdered(byChild: "businessName").observe(.childAdded, with: { (snapshot) in
let key = snapshot.key
if(key == self.loggedInUser?.uid) {
print("Same as logged in user, so don't show!")
} else {
if let locationValue = snapshot.value as? [String: AnyObject] {
let lat = Double(locationValue["businessLatitude"] as! String)
let long = Double(locationValue["businessLongitude"] as! String)
let businessLocation = CLLocation(latitude: lat!, longitude: long!)
let latitude = self.locationManager.location?.coordinate.latitude
let longitude = self.locationManager.location?.coordinate.longitude
let userLocation = CLLocation(latitude: latitude!, longitude: longitude!)
let distanceInMeters : Double = userLocation.distance(from: businessLocation)
let distanceInMiles : Double = ((distanceInMeters.description as String).doubleValue * 0.00062137)
let distanceLabelText = "\(distanceInMiles.string(2)) miles away"
var singleChildDictionary = locationValue
singleChildDictionary["distanceLabelText"] = distanceLabelText as AnyObject
self.usersArray.append(singleChildDictionary as NSDictionary)
/*
func sortMapItems() {
self.mapItems = self.mapItems.sorted(by: { (b, a) -> Bool in
return self.userLocation.location!.distance(from: a.placemark.location!) > self.userLocation.location!.distance(from: b.placemark.location!)
})
}
*/
}
//insert the rows
self.followUsersTableView.insertRows(at: [IndexPath(row:self.usersArray.count-1,section:0)], with: UITableViewRowAnimation.automatic)
}
}) { (error) in
print(error.localizedDescription)
}
}

First make these changes in your code
singleChildDictionary["distanceInMiles"] = distanceInMiles
Then you can sort it like this:
self.usersArray = self.usersArray.sorted {
!($0["distanceInMiles"] as! Double > $1["distanceInMiles"] as! Double)
}

Related

I get an empty CLLocationCoordinates array when loading data from user defaults

I'm trying to store to UserDefaults an array of CCLocationCoordinates from the tracking portion of my app paired with the name of the tracked route as key, to be able to recall it later on to use it within a function.
The problem is that when I call that function I get the index out of range error. I checked and the array is empty.
As I'm new to user defaults I tried to see other similar posts but they're all about NSUserDefaults and didn't find a solution.
Heres the code for the functions for storing and recalling the array:
func stopTracking2() {
self.trackingIsActive = false
self.trackigButton.backgroundColor = UIColor.yellow
locationManager.stopUpdatingLocation()
let stopRoutePosition = RouteAnnotation(title: "Route Stop", coordinate: (locationManager.location?.coordinate)!, imageName: "Route Stop")
self.actualRouteInUseAnnotations.append(stopRoutePosition)
print(actualRouteInUseCoordinatesArray)
print(actualRouteInUseAnnotations)
drawRoutePolyline() // draw line to show route
// checkAlerts2() // check if there is any notified problem on our route and marks it with a blue circle, now called at programmed checking
saveRouteToUserDefaults()
postRouteToAnalitics() // store route anonymously to FIrebase
}
func saveRouteToUserDefaults() {
// save actualRouteInUseCoordinatesArray : change for function
// userDefaults.set(actualRouteInUseCoordinatesArray, forKey: "\(String(describing: userRoute))")
storeCoordinates(actualRouteInUseCoordinatesArray)
}
// Store an array of CLLocationCoordinate2D
func storeCoordinates(_ coordinates: [CLLocationCoordinate2D]) {
let locations = coordinates.map { coordinate -> CLLocation in
return CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
}
let archived = NSKeyedArchiver.archivedData(withRootObject: locations)
userDefaults.set(archived, forKey: "\(String(describing: userRoute))")
userDefaults.synchronize()
}
func loadRouteFromUserDefaults() {
// gets entry from userRouteArray stored in userDefaults and append them into actualRouteInUseCoordinatesArray
actualRouteInUseCoordinatesArray.removeAll()
actualRouteInUseCoordinatesArray = userDefaults.object(forKey: "\(String(describing: userRoute))") as? [CLLocationCoordinate2D] ?? [CLLocationCoordinate2D]() // here we get the right set of coordinates for the route we are about to do the check on
// load route coordinates from UserDefaults
// actualRouteInUseCoordinatesArray = loadCoordinates()! //error found nil
}
// Return an array of CLLocationCoordinate2D
func loadCoordinates() -> [CLLocationCoordinate2D]? {
guard let archived = userDefaults.object(forKey: "\(String(describing: userRoute))") as? Data,
let locations = NSKeyedUnarchiver.unarchiveObject(with: archived) as? [CLLocation] else {
return nil
}
let coordinates = locations.map { location -> CLLocationCoordinate2D in
return location.coordinate
}
return coordinates
}
}
extension NewMapViewController {
// ALERTS :
func checkAlerts2() {
loadRouteFromUserDefaults() //load route coordinates to check in
// CHECK IF ANY OBSTACLE IS OUN OUR ROUTE BY COMPARING DISTANCES
while trackingCoordinatesArrayPosition != ( (actualRouteInUseCoordinatesArray.count) - 1) {
print("checking is started")
print(actualRouteInUseCoordinatesArray)
let trackingLatitude = actualRouteInUseCoordinatesArray[trackingCoordinatesArrayPosition].latitude
let trackingLongitude = actualRouteInUseCoordinatesArray[trackingCoordinatesArrayPosition].longitude
let alertLatitude = alertNotificationCoordinatesArray[alertNotificationCoordinatesArrayPosition].latitude
let alertLongitude = alertNotificationCoordinatesArray[alertNotificationCoordinatesArrayPosition].longitude
let coordinateFrom = CLLocation(latitude: trackingLatitude, longitude: trackingLongitude)
let coordinateTo = CLLocation(latitude: alertLatitude, longitude: alertLongitude)
let coordinatesDistanceInMeters = coordinateFrom.distance(from: coordinateTo)
// CHECK SENSITIVITY: sets the distance in meters for an alert to be considered an obstacle
if coordinatesDistanceInMeters <= 10 {
print( "found problem")
routeObstacle.append(alertNotificationCoordinatesArray[alertNotificationCoordinatesArrayPosition]) // populate obstacles array
trackingCoordinatesArrayPosition = ( trackingCoordinatesArrayPosition + 1)
}
else if alertNotificationCoordinatesArrayPosition < ((alertNotificationCoordinatesArray.count) - 1) {
alertNotificationCoordinatesArrayPosition = alertNotificationCoordinatesArrayPosition + 1
}
else if alertNotificationCoordinatesArrayPosition == (alertNotificationCoordinatesArray.count - 1) {
trackingCoordinatesArrayPosition = ( trackingCoordinatesArrayPosition + 1)
alertNotificationCoordinatesArrayPosition = 0
}
}
findObstacles()
NewMapViewController.checkCounter = 0
displayObstacles()
}
In the extension you can see the function that uses the array.
Right after the print of the array I get the index out of range error.
Thanks as usual to the community.
After trying various solutions offered I decided to rewrite the whole thing.
So after finding a post on how to code/decode my array to string I decided it was the way to go. It shouldn't be heavy on the system as it's a string that gets saved. Please let me know what you think of this solution.
Thank to #Sh_Khan to point out it was a decoding issue, and to #Moritz to point out I was performing a bad practice.
So the code is:
func storeRoute() {
// first we code the CLLocationCoordinate2D array to string
// second we store string into userDefaults
userDefaults.set(encodeCoordinates(coords: actualRouteInUseCoordinatesArray), forKey: "\(String(describing: NewMapViewController.userRoute))")
}
func loadRoute() {
//first se load string from user defaults
let route = userDefaults.string(forKey: "\(String(describing: NewMapViewController.userRoute))")
print("loaded route is \(route!))")
//second we decode it into CLLocationCoordinate2D array
actualRouteInUseCoordinatesArray = decodeCoordinates(encodedString: route!)
print("decoded route array is \(actualRouteInUseCoordinatesArray))")
}
func encodeCoordinates(coords: [CLLocationCoordinate2D]) -> String {
let flattenedCoords: [String] = coords.map { coord -> String in "\(coord.latitude):\(coord.longitude)" }
let encodedString: String = flattenedCoords.joined(separator: ",")
return encodedString
}
func decodeCoordinates(encodedString: String) -> [CLLocationCoordinate2D] {
let flattenedCoords: [String] = encodedString.components(separatedBy: ",")
let coords: [CLLocationCoordinate2D] = flattenedCoords.map { coord -> CLLocationCoordinate2D in
let split = coord.components(separatedBy: ":")
if split.count == 2 {
let latitude: Double = Double(split[0]) ?? 0
let longitude: Double = Double(split[1]) ?? 0
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
} else {
return CLLocationCoordinate2D()
}
}
return coords
}
Rather than using heavy-weight objectiv-c-ish NSKeyed(Un)Archiver and making a detour via CLLocation I recommend to extend CLLocationCoordinate2D to adopt Codable
extension CLLocationCoordinate2D : Codable {
public init(from decoder: Decoder) throws {
var arrayContainer = try decoder.unkeyedContainer()
if arrayContainer.count == 2 {
let lat = try arrayContainer.decode(CLLocationDegrees.self)
let lng = try arrayContainer.decode(CLLocationDegrees.self)
self.init(latitude: lat, longitude: lng)
} else {
throw DecodingError.dataCorruptedError(in: arrayContainer, debugDescription: "Coordinate array must contain two items")
}
}
public func encode(to encoder: Encoder) throws {
var arrayContainer = encoder.unkeyedContainer()
try arrayContainer.encode(contentsOf: [latitude, longitude])
}
}
and replace the methods to load and save data with
func storeCoordinates(_ coordinates: [CLLocationCoordinate2D]) throws {
let data = try JSONEncoder().encode(coordinates)
UserDefaults.standard.set(data, forKey: String(describing: userRoute))
}
func loadCoordinates() -> [CLLocationCoordinate2D] {
guard let data = UserDefaults.standard.data(forKey: String(describing: userRoute)) else { return [] }
do {
return try JSONDecoder().decode([CLLocationCoordinate2D].self, from: data)
} catch {
print(error)
return []
}
}
storeCoordinates throws it hands over a potential encoding error
Load the data with
actualRouteInUseCoordinatesArray = loadCoordinates()
and save it
do {
try storeCoordinates(actualRouteInUseCoordinatesArray)
} catch { print(error) }
Your problem is that you save it as data and try to read directly without unarchiving , You can try
let locations = [CLLocation(latitude: 123, longitude: 344),CLLocation(latitude: 123, longitude: 344),CLLocation(latitude: 123, longitude: 344)]
do {
let archived = try NSKeyedArchiver.archivedData(withRootObject: locations, requiringSecureCoding: true)
UserDefaults.standard.set(archived, forKey:"myKey")
// read savely
if let data = UserDefaults.standard.data(forKey: "myKey") {
let saved = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [CLLocation]
print(saved)
}
}
catch {
print(error)
}

SWIFT 4.1 Cannot invoke initializer for type 'Double' with an argument list of type '(String?)'

I'm retrieving mapView annotations posted in Firebase to show them on map, but while converting String values for latitude and longitude to recombine them into CLLocationCoordinates2D I get the error. I don't understand why, because in another function I use the same method but getting the values from arrays but I don't get the error. Also on retrieving the data I would like to also use the key value from firebase as initialiser for my annotations. But I get two more errors Use of unresolved identifier 'firebaseKey' and Use of unresolved identifier 'recombinedCoordinate' for initialisers. Here're the function:
func displayAlerts() {
// FIREBASE: Reference
ref = Database.database().reference()
// FIREBASE:Retrieve posts and listen for changes
databaseHandle = ref?.child("Community").child("Alert Notifications").observe(.childAdded, with: { (snapshot) in
let data = snapshot.value as? [String:String]
if let actualData = data {
let dataLatitude = data!["Latitude"]
let dataLongitude = data!["Longitude"]
self.alertIconToDisplay = data!["Description"]
let doubledLatitude = Double(dataLatitude)
let doubledLongitude = Double(dataLongitude)
var recombinedCoordinate = CLLocationCoordinate2D(latitude: doubledLatitude!, longitude: doubledLongitude!)
print("Firebase post retrieved !")
self.dummyFunctionToFoolFirebaseObservers()
}
let dataKey = snapshot.key as? String
if let firebaseKey = dataKey {
print("Longitude DataKey is \(String(describing: dataKey))")
print("Longitude Actual DataKey is \(String(describing: firebaseKey))")
self.dummyFunctionToFoolFirebaseObservers()
}
print("fir long \((snapshot.value!, snapshot.key))")
userAlertAnnotation = UserAlert(type: self.alertIconToDisplay, coordinate: recombinedCoordinate, firebaseKey: firebaseKey)
self.mapView.addAnnotation(self.userAlertAnnotation)
})
}
Here's the annotation model :
class UserAlert: NSObject , MKAnnotation {
var type: String?
var firebaseKey: String?
var coordinate:CLLocationCoordinate2D
init(type:String, coordinate:CLLocationCoordinate2D, firebaseKey: String) {
self.type = type
self.firebaseKey = firebaseKey
self.coordinate = coordinate
}
}
What am I doing wrong here? I understand that the error on the initialisers are because initialisation occurs in key closures, but how then I incorporate all data into initialiser ?
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = MKAnnotationView(annotation: userAlertAnnotation, reuseIdentifier: "") // CHANGE FOR NEW ANNOTATION : FULL DATA
//added if statement for displaying user location blue dot
if annotation is MKUserLocation{
return nil
} else {
annotationView.image = UIImage(named: alertIconToDisplay!) // choose the image to load
let transform = CGAffineTransform(scaleX: 0.27, y: 0.27)
annotationView.transform = transform
return annotationView
}
}
func postAlertNotification() {
// to set next notification id as the position it will have in array ( because first position is 0 ) we use the array.count as value
let latitude = alertNotificationLatitude
let longitude = alertNotificationLongitude
let alertType = alertNotificationType
let post: [String:String] = [//"Date" : date as! String,
//"Time" : time as! String,
"Latitude" : latitude as! String,
"Longitude" : longitude as! String,
"Description" : alertType as! String]
var ref: DatabaseReference!
ref = Database.database().reference()
ref.child("Community").child("Alert Notifications").childByAutoId().setValue(post)
}
The error in the topic says that you can't create a Double from an optional String which is true.
To solve it force unwrap the values for Latitude and Longitude.
But the main issue is a scope issue, all variables used in the initializer must be in the same scope. You can flatten the scope with guard statements:
...
databaseHandle = ref?.child("Community").child("Alert Notifications").observe(.childAdded, with: { (snapshot) in
defer { self.dummyFunctionToFoolFirebaseObservers() }
guard let data = snapshot.value as? [String:String] else { return }
guard let firebaseKey = snapshot.key as? String else { return }
// let date = data!["Date"]
// let time = data!["Time"]
let dataLatitude = data["Latitude"]!
let dataLongitude = data["Longitude"]!
self.alertIconToDisplay = data["Description"]!
let doubledLatitude = Double(dataLatitude)
let doubledLongitude = Double(dataLongitude)
let recombinedCoordinate = CLLocationCoordinate2D(latitude: doubledLatitude!, longitude: doubledLongitude!)
print("Firebase post retrieved !")
// self .keyaLon = dataKey
// self.keyaLonArray.append(firebaseKey)
print("Longitude Actual DataKey is \(String(describing: firebaseKey))")
print("fir long \((snapshot.value!, snapshot.key))")
self.userAlertAnnotation = UserAlert(type: self.alertIconToDisplay, coordinate: recombinedCoordinate, firebaseKey: firebaseKey)
self.mapView.addAnnotation(self.userAlertAnnotation)
})

include local on map with firebase swift - error

I am encountering great difficulties in putting an integrated map based on firebase in my project, I am looking for expert knowledge to help me, even though I build run correctly at the time of running the system for, my code below:
Thanks
#IBOutlet var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
let locationsRef = Database.database().reference(withPath: "locations")
locationsRef.observe(.value, with: { snapshot in
for item in snapshot.children {
guard let locationData = item as? DataSnapshot else { continue }
var locationValue = locationData.value as! [String: Any]
var location: CLLocationCoordinate2D!
if let lat = locationValue["lat"] as? String {
let lng = Double(locationValue["lng"] as! String)!
location = CLLocationCoordinate2D(latitude: Double(lat)!, longitude: lng)
} else {
let lat = locationValue["lat"] as! Double
let lng = locationValue["lng"] as! Double
location = CLLocationCoordinate2D(latitude: lat, longitude: lng)
}
func addAnnotations(coords: [CLLocation]){
for coord in coords{
let CLLCoordType = CLLocationCoordinate2D(latitude: coord.coordinate.latitude,
longitude: coord.coordinate.longitude);
let anno = MKPointAnnotation();
anno.coordinate = CLLCoordType;
self.mapView.addAnnotation(anno);
}
}
}
})
}

MapBox swift 3 api

I use MapBox api swift 2.3 and reading geojson. But after swift 3 dont work began to upgrade.
Swift 2.3 examples; its work
for location in locations {
nate2D(latitude: location[1].doubleValue, longitude: location[0].doubleValue)
coordinates.append(coordinate)
}
Swift 3.0 dont work
if let feature = feature as? NSDictionary {
if let geometry = feature["geometry"] as? NSDictionary {
if geometry["type"] as? String == "Polygon" {
var coordinates: [CLLocationCoordinate2D] = []
if let locations = geometry["coordinates"] as? NSArray {
for location in locations {
for i in (0 ..< (location as AnyObject).count)
{
let coordinate = CLLocationCoordinate2D(latitude: ???, longitude: ???)
coordinates.append(coordinate)
}
}
}
let shape = MGLPolygon(coordinates: &coordinates, count: UInt(coordinates.count))
DispatchQueue.main.async(execute: {
[unowned self] in
self.mapView.addAnnotation(shape)
})
}
}
}
enter image description here
It ' s correct answer
for location in locations {
for i in (0 ..< (location as AnyObject).count)
{
let a = locations[0] as? NSArray
let b = a?[i] as? NSArray
var c = b?[0]
print(c);
let coordinate = CLLocationCoordinate2D(latitude: b?[1] as! CLLocationDegrees, longitude: b?[0] as! CLLocationDegrees)
coordinates.append(coordinate)
}
}

Could not cast value of type 'NSTaggedPointerString' to 'NSArray in Google Place Api

i integrated Google Place Api to fetch user location, i have to fetch "area","state" and "City" while i try to get value my App gets crash. Please post ur answer
here my sample code
func mapView(mapView: GMSMapView, idleAtCameraPosition position: GMSCameraPosition)
{
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let centerLocation = CLLocation(latitude: position.target.latitude, longitude: position.target.longitude)
CLGeocoder().reverseGeocodeLocation(centerLocation, completionHandler:
{(placemarks, error) in
if (error == nil && placemarks!.count>0)
{
let placemark = CLPlacemark(placemark: placemarksArray[0] as! CLPlacemark)
let latitude = String(format: "%.8f",position.target.latitude)
let longitude = String(format: "%.8f",position.target.longitude)
if let addrList = placemark.addressDictionary
{
print("address==\(address)")
let addStr = address?["FormattedAddressLines"] as! [String]
let addStr1 = address?["City"] as! [String]
let addStr2 = address?["State"] as! [String]
let addStr3 = address?["SubLocality"] as! [String]
NSLog("%#\n%#\n%#", addStr1,addStr2,addStr3)
self.addressLabel.text = addStr.joinWithSeparator(",")
}
}
})
})
}
my output :
address=={
City = "New Delhi";
Country = India;
CountryCode = IN;
FormattedAddressLines = (
"Mayur Vihar",
"New Delhi",
"Delhi 110092",
India
);
Name = 110092;
State = Delhi;
SubAdministrativeArea = Delhi;
SubLocality = "Mayur Vihar";
ZIP = 110092;
}
and my Crash Report :
Could not cast value of type 'NSTaggedPointerString' (0x1064cb860) to 'NSArray' (0x1064cb900).
City and State has String value and FormattedAddressLines has Arry.
Use like this
let addStr1 = address["City"] as! String
let addStr2 = address["State"] as! String
let FormattedAddressLines = address["FormattedAddressLines"] as![String]

Resources