Firebase GeoFire closure - swift

I'm trying Firebase GeoFire and closures always trip me up. Firebase provides this sample code snippet in their documentation
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query.getDocuments(completion: getDocumentsCompletion)
}
Unfortunately it doesn't demonstrate just what I need...
I want this to fire on pre set interval so I have a scheduled it like this:
locationTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(getLocations), userInfo: nil, repeats: true)
Then my code is pretty much as per the sample:
var matchingDocs = [QueryDocumentSnapshot]()
#objc func getLocations() {
let center = CLLocationCoordinate2D(latitude: lat, longitude: lon)
let radiusInM: Double = 10000
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInM)
let queries = queryBounds.map { bound -> Query in
return db.collection("geopoints")
.order(by: "geohash")
}
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
if distance <= radiusInM {
matchingDocs.append(document)
}
}
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query.getDocuments(completion: getDocumentsCompletion)
}
}
}
What is the proper way to wait for the callbacks to complete, and continue executing the program outside the getLocations() function with matchingDocs containing all the geopoints? Thanks for any recommendations.

This is my solution. Function allDone() with the completion will be called only after all queries come back.
// button press to read data from Firebase
#IBAction func getData(_ sender: Any) {
getLocations(completion: {
// code here to process all query results
})
}
func getLocations(completion: #escaping () -> Void) {
let center = CLLocationCoordinate2D(latitude: lat, longitude: lon)
let radiusInM: Double = 10000
var queriesFinished = 0
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInM)
let queries = queryBounds.map { bound -> Query in
return db.collection("geopoints")
.order(by: "geohash")
}
let numOfQueries = queries.count
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
if distance <= radiusInM {
matchingDocs.append(document)
}
}
queriesFinished += 1
allDone()
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query.getDocuments(completion:getDocumentsCompletion)
}
func allDone() {
if queriesFinished == numOfQueries {
completion()
}
}
}
I was reading that async/await may be a better way but did not get it to work.

Related

GeoFire Firebase Swift in realtime

Please how can make it in real time ?
like adding addSnapshotListener.
This code is only work when it called, I wanna if you change in FireBase, the reflect will be on the frontend.
reference >>> https://firebase.google.com/docs/firestore/solutions/geoqueries
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInM: Double = 50 * 1000
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInM)
let queries = queryBounds.map { bound -> Query in
return db.collection("cities")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but
// most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
if distance <= radiusInM {
matchingDocs.append(document)
}
}
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query.getDocuments(completion: getDocumentsCompletion)
}
Thanks
If you want to listen for realtime updates, you can use .addSnapshotListener instead of getDocuments. Since this is not directly related to the geoquery, the solution documents the simpler approach - but the code for geoquerying would work exactly the same.

How to get Documents out of an geo query?

I used this function for an geo query. But I don't known how to add the document from the query to an array. So I can display some Map Annotations with infos from an Firestore document. How should I change it?
func geoQuery() {
// [START fs_geo_query_hashes]
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInKilometers: Double = 50
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInKilometers)
let queries = queryBounds.compactMap { (any) -> Query? in
guard let bound = any as? GFGeoQueryBounds else { return nil }
return db.collection("cities")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but
// most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
if distance <= radiusInKilometers {
matchingDocs.append(document)
}
}
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query.getDocuments(completion: getDocumentsCompletion)
}
// [END fs_geo_query_hashes]
}
https://firebase.google.com/docs/firestore/solutions/geoqueries?hl=en#swift_2 This is the Firebase documentary.
I don't know how your documents are structured or how your map is configured to display data (annotations versus regions, for example), but the general fix for your problem is to coordinate the loop of queries in your function and give them a completion handler. And to do that, we can use a Dispatch Group. In the completion handler of this group, you have an array of document snapshots which you need to loop through to get the data (from each document), construct the Pin, and add it to the map. There are a number of other steps involved here that I can't help you with since I don't know how your documents and map are configured but this will help you. That said, you could reduce this code a bit and make it more efficient but let's just go with the Firebase sample code you're using and get it working first.
struct Pin: Identifiable {
let id = UUID().uuidString
var location: MKCoordinateRegion
var name: String
var img: String
}
func geoQuery() {
// [START fs_geo_query_hashes]
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInKilometers: Double = 50
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInKilometers)
let queries = queryBounds.compactMap { (Any) -> Query? in
guard let bound = Any as? GFGeoQueryBounds else { return nil }
return db.collection("cities")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
// Create a dispatch group outside of the query loop since each iteration of the loop
// performs an asynchronous task.
let dispatch = DispatchGroup()
var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
dispatch.leave() // leave the dispatch group when we exit this completion
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let name = document.data()["names"] as? String ?? "no name"
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but
// most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
if distance <= radiusInKilometers {
matchingDocs.append(document)
}
}
dispatch.leave() // leave the dispatch group when we exit this completion
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
dispatch.enter() // enter the dispatch group on each iteration
query.getDocuments(completion: getDocumentsCompletion)
}
// [END fs_geo_query_hashes]
// This is the completion handler of the dispatch group. When all of the leave()
// calls equal the number of enter() calls, this notify function is called.
dispatch.notify(queue: .main) {
for doc in matchingDocs {
let lat = doc.data()["lat"] as? Double ?? 0
let lng = doc.data()["lng"] as? Double ?? 0
let name = doc.data()["names"] as? String ?? "no name"
let coordinates = CLLocation(latitude: lat, longitude: lng)
let region = MKCoordinateRegion(center: <#T##CLLocationCoordinate2D#>, latitudinalMeters: <#T##CLLocationDistance#>, longitudinalMeters: <#T##CLLocationDistance#>)
let pin = Pin(location: region, name: name, img: "someImg")
// Add pin to array and then to map or just add pin directly to map here.
}
}
}
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, annotationItems: pvm.allPins) { pin in
MapAnnotation(coordinate: pin.location.coordinate) {
Image(pin.img)
}
}

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.

Calculate each distance from user to several points by route in Swift

I'm a newbie in swift and I'm trying to calculate the distance, on route, from userLocation to several Point of interest.
I don’t want, in this part of the app, “draw” the route on a map, but I only want to calculate the distance on route instead the distance between two coordinate and then show this distance as information inside the callout on the map.
The coordinate of the POI (latitude and longitude) are contained in a database.
I have read some threads about this argument here on Stack Overflow:
Measuring distance in meters from a drawn route in MKMapView
MKMapView get distance between coordinates on customized route
and other tutorials:
http://www.technetexperts.com/mobile/draw-route-between-2-points-on-map-with-ios7-mapkit-api/
https://videos.raywenderlich.com/courses/mapkit-and-core-location/lessons/9
then i wrote this code:
for item in items {
if item.latitudine != "" && item.longitudine != "" {
// Point Of Interest coordinate
let latitude = Double(item.latitude!)
let longitude = Double(item.longitude!)
let itemLocation = CLLocation(latitude: latitude!, longitude: longitude!)
let itemLocationPlacemark = MKPlacemark(coordinate: itemLocation.coordinate, addressDictionary: nil)
// user coordinate
let userLocation = CLLocation(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude)
let userLocationPlacemark = MKPlacemark(coordinate: userLocation.coordinate, addressDictionary: nil)
// create Request object
let request = MKDirectionsRequest()
request.source = MKMapItem(placemark: userLocationPlacemark)
request.destination = MKMapItem(placemark: itemLocationPlacemark)
request.requestsAlternateRoutes = false
request.transportType = .automobile
let directions = MKDirections(request: request)
directions.calculate {
[weak self] (response, error) in
if error == nil {
for route in (response?.routes)! {
let distance = (route.distance)/1000
print(distance)
}
}
}
}
}
The problem is when I execute the code from the line directions.calculate.
The program run the line but then don’t execute the rest, don’t execute the control if error == nil and the instructions in the closure.
So now I wonder if my idea is wrong and, if not, how can obtain my goal.
(Posted solution on behalf of the OP).
Reading other threads I understand that the problem was the closure inside che for loop. So after several attempts I found the solution that work for me. I write it here so that can be useful to someone else:
var counter: Int!
...
for item in itemPin {
if item.latitude != "" && item.longitude != "" {
....
....
let directions = MKDirections(request: request)
directions.calculate {
(response, error) in
print("error \(error)")
if error == nil {
counter = self.counter + 1
print(self.counter)
if self.counter >= self.itemPin.count {
let result = (response?.routes[0].distance)!/1000
}
}
}
...
}
}

Issue persisting MKPolygon points using Parse framework

I have the following method that saves points to Parse.com using the PFGeoPoint object:
I capture them using:
func convertPoint(touch: UITouch) {
let location = touch.locationInView(self.mapView) as CGPoint
let coordinate: CLLocationCoordinate2D = self.mapView.convertPoint(location, toCoordinateFromView: self.mapView)
self.coordinates.addObject(NSValue(MKCoordinate: coordinate))
}
Then I use the following to add them to Parse:
func addPolygonToMap() {
let HUD: MBProgressHUD = showActivityIndicator(true, self.view)
var numberOfPoints: NSInteger = self.coordinates.count
if (numberOfPoints > 4) {
var points: [CLLocationCoordinate2D] = []
// Save to Parse object.
var geofenceUserObject = PFObject(className: "GeofenceUser")
let geofenceId = self.generateUUID()
geofenceUserObject["UserId"] = "IjlpQHwyfG"
geofenceUserObject["GeofenceId"] = geofenceId
geofenceUserObject.saveInBackgroundWithBlock({ (succeeded: Bool, error: NSError!) in
if (error != nil) {
println("Error saving: \(error)")
} else if (succeeded) {
for i in 0..<numberOfPoints {
let coordinateValue = self.coordinates[i].MKCoordinateValue
points.insert(coordinateValue, atIndex: i)
var geoPoint = PFGeoPoint(latitude: coordinateValue.latitude, longitude: coordinateValue.longitude)
var geofenceObject = PFObject(className: "GeofenceCoordinates")
geofenceObject["Point"] = geoPoint
geofenceObject["GeofenceId"] = geofenceId
geofenceObject.saveInBackgroundWithBlock({ (operation, error) in
println("Saved Geofence objects: \(operation)")
println("Points: \(numberOfPoints)")
println("Index: \(i+1)")
if (i+1 == numberOfPoints) {
self.polygon = MKPolygon(coordinates: &points, count: numberOfPoints)
self.mapView.addOverlay(self.polygon)
self.isDrawingPolygon = false
self.createDrawButton("DRAW", color: UIColor(red: 11/255, green: 188/255, blue: 185/255, alpha: 1))
self.canvasView.image = nil
self.canvasView.removeFromSuperview()
HUD.hide(true)
}
})
}
}
})
}
}
This is an example MKpolygon that gets created (approx. 319 points):
This is the method I'm using to add the points to the map:
var query = PFQuery(className: "GeofenceCoordinates")
query.orderByAscending("createdAt")
query.whereKey("GeofenceId", equalTo: geofenceId)
query.findObjectsInBackgroundWithBlock({ (objects, error) in
if let objects = objects as? [PFObject] {
var coordinates: Array<CLLocationCoordinate2D> = []
for point in objects {
if let coordinate = point.objectForKey("Point") as? PFGeoPoint {
let c = CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude)
coordinates.append(c)
}
}
let polygon = MKPolygon(coordinates: &coordinates, count: coordinates.count)
self.mapView.addOverlay(polygon)
}
})
The problem is that when I retrieve these points from Parse I get the following MKPolygon instead, which is missing quite a few points and looks incomplete.
I'm not quite sure whether it's my way of persisting the data or the way I'm retrieving the data.
Okay, so I completely refactored the way I was saving coordinate data to Parse.
I changed the column to be an Array instead of a GeoPoint and it
Saves about 300% faster.
Retrieves all the coordinates properly.