Sort By Distance In TableView - ios

I'm trying to sort the results of my tableView by distance closest to the user. I've separated my distance and function to populate distance to get a better hold of my data manipulation. I feel like it should be simple but, I know I'm missing it.
The distance function is this:
func calculateDistance(userlat: CLLocationDegrees, userLon:CLLocationDegrees, venueLat:CLLocationDegrees, venueLon:CLLocationDegrees) -> Double {
let userLocation:CLLocation = CLLocation(latitude: userlat, longitude: userLon)
let priceLocation:CLLocation = CLLocation(latitude: venueLat, longitude: venueLon)
//let distance = String(format: "%.0f", userLocation.distance(from: priceLocation)/1000)
return userLocation.distance(from: priceLocation)/1000
}
PopulateData function:
func populateData() {
//Pulls TableData for UITableView
DataService.ds.REF_VENUE.observe(.value, with: { (snapshot) in
self.posts = [] // THIS IS THE NEW LINE
if snapshot.exists(){
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] {
for snap in snapshot {
if let snapValue = snap.value as? [String:AnyObject],
let venueLat = snapValue["LATITUDE"] as? Double,
let venueLong = snapValue["LONGITUDE"] as? Double
{
let distance = self.calculateDistance(userlat: self.userLatt, userLon: self.userLonn, venueLat: venueLat, venueLon: venueLong)
if distance <= 2 {
let key = snap.key
let post = Post(postKey: key, postData: snapValue)
self.posts.append(post)
self.posts.sort(by: (posts, distance)) //Where I'm trying to sort
}
}
}
self.getDataForMapAnnotation()
}
self.tableView.reloadData()
}
})
}
I'm not sure if I can even sort an array of dictionaries but, the end goal is to have the tableView show the venues closest to the user. If you have any suggestions, please let me know!

If you added the distance you calculate as a property of your Post class/struct, sorting the posts array by distance would be pretty straightforward. Using the Swift shorthand syntax for closures, your sort function could look something like this:
self.posts.sort {
return $0.distance < $1.distance
}
This would sort the posts array by distance in ascending order.

Related

iOS -Using Pull-To-Refresh How to know when new data is added to Firebase Node based on User Location

My app lets users sell things like sneakers etc. The sneakers that appears in the user's feed is based on the sellers who has posted items that are nearby to the user. I use GeoFire to get the seller's location and everything works fine. When the user uses pullToRefresh if there isn't any new data/sneakers that have been added nearby then there is no need to refresh the list.
The place where I am stumped is when the user pullsToRefresh, how do I determine that new items have been added by either a completely new seller who is nearby or the the same seller's who have added additional pairs of sneakers?
For eg. userA lives in zip code 10463 and there are 2 seller's within a 20 mi radius. Any sneakers that those seller's have for sale will appear in the user's feed. But a 3rd seller can come along and post a pair of sneakers or any of the first 2 seller's can add an additional pair. If the user pullsToRefesh then those items will appear but if nothing is added then pullToRefresh shouldn't do anything.
I don't want to unnecessarily rerun firebase code if I don't have to. The only way to do that would be to first check the postsRef to check to see if any new sneakers were added by the 2 sellers or a completely new seller who is also nearby.
code:
let refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
return refreshControl
}()
#objc func pullToRefresh() {
// if there aren't any new nearby sellers or current sellers with new items then the two lines below shouldn't run
arrOfPosts.removeAll() // this is the array that has the collectionView's data. It gets populated in thirdFetchPosts()
firstGetSellersInRadius(miles: 20.0)
}
override func viewDidLoad() {
super.viewDidLoad()
firstGetSellersInRadius(miles: 20.0) // the user can change the mileage but it's hardcoded for this example
}
// 1. I get the user's location and then get all the nearby sellers
func firstGetSellersInRadius(miles: Double) {
// user's location
guard let currentLocation = locationManager.location else { return }
let lat = currentLocation.coordinate.latitude
let lon = currentLocation.coordinate.longitude
let location = CLLocation(latitude: lat, longitude: lon)
let radiusInMeters = (miles * 2) * 1609.344 // 1 mile == 1609.344 meters
let region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: radiusInMeters, longitudinalMeters: radiusInMeters)
let geoFireRef = Database.database().reference().child("geoFire")
regionQuery = geoFireRef?.query(with: region)
queryHandle = regionQuery?.observe(.keyEntered, with: { (key: String!, location: CLLocation!) in
let geoModel = GeoModel()
geoModel.userId = key
geoModel.location = location
self.arrOfNearbySellers.append(geoModel)
})
regionQuery?.observeReady({
self.secondLoopNearbySellersAndGetTheirAddress(self.arrOfNearbySellers)
})
}
// 2. I have to grab the seller's username and profilePic before I show their posts because they're shown along with the post
func secondLoopNearbySellersAndGetTheirAddress(_ geoModels: [GeoModel]) {
let dispatchGroup = DispatchGroup()
for geoModel in geoModels {
dispatchGroup.enter()
if let userId = geoModel.userId {
let uidRef = Database.database().reference().child("users")?.child(userId)
uidRef.observeSingleEvent(of: .value, with: { [weak self](snapshot) in
guard let dict = snapshot.value as? [String: Any] else { dispatchGroup.leave(); return }
let profilePicUrl = dict["profilePicUrl"] as? String
let username = dict["username"] as? String
let userModel = UserModel()
userModel.profilePicUrl = profilePicUrl
userModel.username = username
self?.arrOfSellers.append(userModel)
dispatchGroup.leave()
})
}
}
dispatchGroup.notify(queue: .global(qos: .background)) { [weak self] in
self?.thirdFetchPosts(self!.arrOfSellers)
}
}
// 3. now that I have their address I fetch their posts
func thirdFetchPosts(_ userModels: [UserModel]) {
let dispatchGroup = DispatchGroup()
var postCount = 0
var loopCount = 0
for userModel in userModels {
dispatchGroup.enter()
if let userId = userModel.userId {
let postsRef = Database.database().reference().child("posts")?.child(userId)
postsRef?.observe( .value, with: { [weak self](snapshot) in
postCount = Int(snapshot.childrenCount)
guard let dictionaries = snapshot.value as? [String: Any] else { dispatchGroup.leave(); return }
dictionaries.forEach({ [weak self] (key, value) in
print(key, value)
loopCount += 1
guard let dict = value as? [String: Any] else { return }
let postModel = PostModel(userModel: userModel, dict: dict)
self?.arrOfPosts.append(postModel)
if postCount == loopCount {
dispatchGroup.leave()
postCount = 0
loopCount = 0
}
})
})
}
}
dispatchGroup.notify(queue: .global(qos: .background)) { [weak self] in
self?.fourthRemoveQueryObserverReloadCollectionView()
}
}
// 4. now that I have all the posts inside the arrOfPosts I can show them in the collectionView
func foutrhRemoveQueryObserverReloadCollectionView() {
DispatchQueue.main.async { [weak self] in
self?.arrOfPosts.sort { $0.postDate ?? 0 > $1.postDate ?? 0}
self?.refreshControl.endRefreshing()
if let queryHandle = self?.queryHandle {
self.regionQuery?.removeObserver(withFirebaseHandle: queryHandle)
}
self?.collectionView.reloadData()
self?.arrOfNearbySellers.removeAll()
self?.arrOfSellers.removeAll()
}
}

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 Firebase Sort By Distance

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

Remove Object From Array Using Enumeration

I have an array of dictionaries. Each dictionary contains latitude and longitude so I'm getting the distance of each item from the current user location. If the distance in miles is greater than 20, that particular dictionary should be removed from the array. If the dictionary is not removed from the array, an annotation is created and added to an annotation array which is then used to add annotations to a map once the enumeration is finished. I'm only getting one annotation added when I should be getting three so I know I'm doing something wrong in my enumeration.
func checkDistanceAndAddPins() {
for gym in gyms {
var index = 0
let gymLatitude = gym["latitude"]!!.doubleValue
let gymLongitude = gym["longitude"]!!.doubleValue
let gymLocation = CLLocation(latitude: gymLatitude, longitude: gymLongitude)
let distance = gymLocation.distanceFromLocation(myLocation!)
let distanceInMeters = NSNumber(double: distance)
let metersDouble = distanceInMeters.doubleValue
let miles = metersDouble * 0.00062137
if miles > maxDistance {
gyms.removeAtIndex(index)
} else {
let location = CLLocationCoordinate2D(latitude: gymLatitude, longitude: gymLongitude)
gymAnnotation.title = gym["Name"] as? String
gymAnnotation.subtitle = gym["Address"] as? String
gymAnnotation.coordinate = location
gymAnnotation.gymPhoneNumber = gym["Phone"] as? String
if let website = gym["Website"] as? String {
gymAnnotation.gymWebsite = website
}
gymLocations.append(gymAnnotation)
}
index += 1
}
dispatch_async(dispatch_get_main_queue()) {
self.gymMap.addAnnotations(self.gymLocations)
}
}

How to update Google Maps marker position in swift/iOS

I would like to have multiple markers and update their position, based on new data from server. This is how I show marker on map (basic stuff):
func showMarkerOnMap() {
mapView.clear() //<- That's GMSMapView
for all in dataFromServer {
let lat5 = all.latitude
let lon5 = all.longitude
let position = CLLocationCoordinate2DMake(lat5, lon5)
let marker = GMSMarker(position: position)
marker.flat = true
marker.map = self.mapView
}
}
This is how I get dataFromServer using Alamofire:
var dataFromServer = [dataClass]()
func getCoordinatesFromServer(){
//Here goes headers and authentication data
Alamofire.request(.GET, URL, headers: headers).authenticate(user: oranges, password: appels).responseJSON { response in
switch response.result {
case .Success:
//Remove existing dataFromServer
self.dataFromServer.removeAll()
if let value = response.result.value {
let json = JSON(value)
for result in json.arrayValue {
let lat = result["latitude"].doubleValue
let lon = result["longitude"].doubleValue
let zip = dataClass(latitude: lat, longitude: lon)
//The next part is for checking that coordinates do not overlap
if self.dataFromServer.count < 1 {
self.dataFromServer.append(zip)
} else {
for all in self.dataFromServer {
guard all.latitude != lat else {
return
}
self.trblInfo1.append(zip)
}
}
}
//This should update existing markers?
self.showMarkerOnMap()
}
case .Failure(let error):
print(error)
}
}
}
Basically I just append all received data to my dataFromServer which belongs to dataClass class:
class dataClass: NSObject {
var latitude: Double
var longitude: Double
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
My getCoordinatesFromServer() function is being called every 3 seconds (for now). What I was expecting to receive new coordinates (and I do receive them for sure), thenshowMarkerOnMap() should be called thus clearing all existing markers and creating news. What I get - marker duplicate and noticeable lag. The original marker disappears if go to another View and then comeback to View containing mapView.
Any suggestion on how to improve my code or some alternative?
If you have any kind of unique identifier for your positions that came from server, you can keep a list of markers and then update their location when new data arrive. Something like this:
for result in json.arrayValue {
let lat = result["latitude"].doubleValue
let lon = result["longitude"].doubleValue
let identifier = result["identifier"] as! String
self.myMarkersDictionary[identifier]?.position.latitude = lat
self.myMarkersDictionary[identifier]?.position.longitude = lon
...
}

Resources