When to use compound queries when using GeoHash? - swift

I need to fetch all users within x radius that match the preferences of the current user.
So I started implementing geohash from Firebase, the documentation was great, my problem might be silly but I don't know when to use all my compound queries...(whereField)
I'm specifying them in the "for query in queries" below but I have the feeling It's not the right spot. how do you use compound queries when using geohash?
//MARK: - GET MATCHES WITH GEOHASH
func getMatchesNearMe(radius: Double) {
// Find matches within 50km of my location
let user = UserService.shared.user
let center = CLLocationCoordinate2D(latitude: user.latitude ?? 0, longitude: user.longitude ?? 0)
let radiusInKilometers: Double = radius
// 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("users")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
var matchingDocs = [Matches]()
// 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
}
print("\nDocs: Count \(documents.count)")
for doc in snapshot!.documents {
var m = Matches()
m.latitude = doc.data()["latitude"] as? Double ?? 0
m.longitude = doc.data()["longitude"] as? Double ?? 0
let coordinates = CLLocation(latitude: m.latitude ?? 0, longitude: m.longitude ?? 0)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
m.id = doc.data()["id"] as? String ?? ""
m.name = doc.data()["name"] as? String ?? ""
m.birthdate = doc.data()["birthdate"] as? Date ?? Date()
m.gender = doc.data()["gender"] as? String ?? ""
m.datingPreferences = doc.data()["datingPreferences"] as? String ?? ""
m.height = doc.data()["height"] as? Int ?? 0
m.imageUrl1 = doc.data()["photo1"] as? String ?? ""
m.imageUrl2 = doc.data()["photo2"] as? String ?? ""
m.imageUrl3 = doc.data()["photo3"] as? String ?? ""
m.imageUrl4 = doc.data()["photo4"] as? String ?? ""
m.imageUrl5 = doc.data()["photo5"] as? String ?? ""
m.imageUrl6 = doc.data()["photo6"] as? String ?? ""
m.Q1day2live = doc.data()["Q1day2live"] as? String ?? ""
m.QlotteryWin = doc.data()["QlotteryWin"] as? String ?? ""
m.QmoneynotanIssue = doc.data()["QmoneynotanIssue"] as? String ?? ""
m.bucketList = doc.data()["bucketList"] as? String ?? ""
m.jokes = doc.data()["jokes"] as? String ?? ""
// 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)
print("MatchName: \(m.name), distance: \(distance) \tlat: \(m.latitude), \(m.longitude)")
if distance <= radiusInKilometers {
matchingDocs.append(m)
}
} //end for loop
self.matches = matchingDocs
self.usersLoaded = true
}
// 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
.whereField("gender", in: ["Women", "men"])
.whereField("conversations", notIn: [user.name])
//.getDocuments(completion: getDocumentsCompletion)
.addSnapshotListener(getDocumentsCompletion)
}
print("Docs: \(matchingDocs.count)")
}

If you want to add additional conditions to the query you got from GeoFire, you can do so here:
return db.collection("users")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
.whereField("gender", isEqualTo: "female")
You may need to add an index for this, so be sure to check the log output for error messages around that (and a link to the Firebase console to quickly create the index).

Related

Fetch new comments from firebase using a query

I have a handleRefresh() function that is called when the user refreshes the page. When the refresh happens new comments that were posted are loaded into the tableview.
The problem i have is that when the users refreshes the data in the tableview loads twice so i get the updated comments but with duplicates where it has reloaded the old comments again.
I am a bit stuck on how to fix this.
Here is the code.
var CommentsQuery: DatabaseQuery {
let postRef = Database.database().reference().child("posts")
let postKey = keyFound
let postCommentRef = postRef.child(postKey)
let lastComment = self.comments.last
var queryRef: DatabaseQuery
if lastComment == nil {
queryRef = postCommentRef.queryOrdered(byChild: "timestamp")
} else {
let lastTimestamp = lastComment!.createdAt.timeIntervalSince1970 * 1000
queryRef = postCommentRef.queryOrdered(byChild: "timestamp").queryEnding(atValue: lastTimestamp)
}
return queryRef
}
#objc func handleRefresh() {
CommentsQuery.queryLimited(toLast: 20).observeSingleEvent(of: .value) { snapshot in
var tempComments = [Comments]()
let commentsSnap = snapshot.childSnapshot(forPath: "comments")
let allComments = commentsSnap.children.allObjects as! [DataSnapshot]
for commentSnap in allComments {
let degree = commentSnap.childSnapshot(forPath: "reply degree").value as? String ?? ""
let name = commentSnap.childSnapshot(forPath: "reply name").value as? String ?? ""
let text = commentSnap.childSnapshot(forPath: "reply text").value as? String ?? ""
let university = commentSnap.childSnapshot(forPath: "reply university").value as? String ?? ""
let photoURL = commentSnap.childSnapshot(forPath: "reply url").value as? String ?? ""
let url = URL(string: photoURL)
let timestamp = commentSnap.childSnapshot(forPath: "timestamp").value as? Double
let lastComment = self.comments.last
if snapshot.key == lastComment?.id {
let newComments = Comments(id: snapshot.key, fullname: name, commentText: text, university: university, degree: degree, photoURL: photoURL, url: url!, timestamp: timestamp!)
tempComments.insert(newComments, at: 0)
print("fetchRefresh")
}
}
self.comments.insert(contentsOf: tempComments, at: 0)
self.fetchingMore = false
self.refreshControl.endRefreshing()
self.tableView.reloadData()
}
}
If the self.comments.last is persisted across page reloads, then it seems to be that the problem is that you use queryEnding(atValue: here:
queryRef = postCommentRef.queryOrdered(byChild: "timestamp").queryEnding(atValue: lastTimestamp)
Since timestamp values are incremental (higher values are newer), you want the node with a timestamp higher than the latest value, which you do with queryStarting(atValue: and not with queryEnding(atValue:.

Display Only Mapkit Annotations From Firebase Created Within The Last Hour

I am currently able to pull down all of my annotations from Firebase Firestore and display them with no problem. In my snapshot listener I would like to be able to only display annotations created within the last hour. I've included my code below that isn't working and is still returning all annotations in my Firestore.
let hourAgo = Date().addingTimeInterval(-3600)
db.collection("pins").addSnapshotListener { QuerySnapshot, Error in
guard let documents = QuerySnapshot?.documents else {
print("No documents")
return
}
let annotation = documents.map { QueryDocumentSnapshot -> Pin in
let data = QueryDocumentSnapshot.data()
let latitude = data["latitude"] as? Double ?? 0.0
let longitude = data["longitude"] as? Double ?? 0.0
let eventTitle = data["eventTitle"] as? String ?? ""
let eventSubtitle = data["eventSubtitle"] as? String ?? ""
let lastUpdated = data["lastUpdated"] as? Timestamp ?? Timestamp.init()
let pinDate = lastUpdated.dateValue()
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
if pinDate >= self.hourAgo {
self.displayPins(coordinate: coordinate, eventSubtitle: eventSubtitle, eventTitle: eventTitle)
}else{
return Pin(latitude: latitude, longitude: longitude, eventTitle: eventTitle, eventSubtitle: eventSubtitle)
}
//print(annotation)
return Pin(latitude: latitude, longitude: longitude, eventTitle: eventTitle, eventSubtitle: eventSubtitle)
}
}
}
func displayPins(coordinate: CLLocationCoordinate2D, eventSubtitle: String, eventTitle: String) {
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotation.title = eventTitle
annotation.subtitle = eventSubtitle
mapView.addAnnotation(annotation)
}```

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

Cannot subscript a value of type '[Any]' with an index of type 'String'

I am retrieving data from firestore and so far everything is successful. This is the result in my console.
[{
Name = Tuna;
Price = "$3.6"; }, {
Name = Snapper;
Price = "$25.60"; }]
I want to store Name and Price into variables so that I can display them in my table view. As shown in the code bellow when i set nameOfItem to the Data2, i get an error that states, "Cannot subscript a value of type '[Any]' with an index of type 'String'". I was wondering if someone can help me fix this!
override func viewWillAppear(_ animated: Bool) {
itemCollectionRef.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
}else {
guard let snap = snapshot else {return}
for document in snap.documents {
let data2 = document.data()["Items"]! as? Array ?? []
print(data2)
let RandomVariable = data2[0]
print(RandomVariable)
let nameOfItem = data2["Name"] as? String ?? ""
let priceOfItem = data2["Price"] as? String ?? ""
//let priceOfItem = data2["Price"] as? String ?? ""
//print(nameOfItem, priceOfItem)
}
}
}
As you can see for yourself there is no such thing as an "Items" element in your console output instead as the error message says you have an array so change your code inside the for loop to
let data = document.data() as? [Any] ?? []
for item in data {
if let dictionary = item as? [String: String] {
let nameOfItem = dictionary["Name"] ?? ""
let priceOfItem = dictionary["Price"] ?? ""
//...
}
}

Accessing data inside a closure after it has been completed

I want to be able to access the results array, after all the data has been added from Firebase to my array. Every time I try this, I get nil array.
Objective is to have a list of location info objects in an array, loaded through Firebase.
My code snippet:
class func loadData(){
let root = FIRDatabase.database().reference()
let locationSummary = root.child("LocSummary")
locationSummary.observe(.childAdded,with: { (snapshot) in
print("inside closure")
let values = snapshot.value as? NSDictionary
let name = values?["Name"] as? String ?? ""
let rating = values?["Rating"] as? Int
let latitude = values?["Latitude"] as? Double
let longitude = values?["Longitude"] as? Double
let musicType = values?["Music"] as? String ?? ""
let loc = LocationInfo.init(name: name, rating: rating!, lat:
latitude!, long: longitude!, musicTyp: musicType)
resultsArray.append(loc)
})
}
Try something like this:
class func loadData(completion: #escaping (_ location: LocationInfo) -> Void) {
let root = FIRDatabase.database().reference()
let locationSummary = root.child("LocSummary")
locationSummary.observe(.childAdded,with: { (snapshot) in
print("inside closure")
let values = snapshot.value as? NSDictionary
let name = values?["Name"] as? String ?? ""
let rating = values?["Rating"] as? Int
let latitude = values?["Latitude"] as? Double
let longitude = values?["Longitude"] as? Double
let musicType = values?["Music"] as? String ?? ""
let loc = LocationInfo.init(name: name, rating: rating!, lat:
latitude!, long: longitude!, musicTyp: musicType)
completion(loc)
})
}
In your cycle add something like this:
func getArray(completion: #escaping (_ yourArray: [LocationInfo]) -> Void {
var resultsArray = [LocationInfo]()
let countOfLoadedItems = 0
for item in yourArrayForCycle { // or your own cycle. Implement your logic
loadData(completion: { location in
countOfLoadedItems += 1
resultsArray.append(location)
if countOfLoadedItems == yourArrayForCycle.count {
completion(resultsArray)
}
})
}
}
Then in function, where you wants your data:
getArray(completion: { result in
yourArrayToFill = result
// reload data etc..
})
Something like this. Adapt it to your solution.
Hope it helps