After doing a query to Firestore i'm attempting to store the Firestore path so that i can acces it later.
This way i can do a Firestore query and attach a listener. On the second query i can access that stored path to detach the old listener and attach a new one.
When initializing a app a query is executed.
override func viewDidLoad() {
super.viewDidLoad()
let path = Firestore.firestore().collection("all_users").document(uid).collection("usercollection").whereField("datefilter", isGreaterThan: self.currentDate)
newSearch(firebasePath: path)
However, storing the path is necessary as the query can change in unpredictable ways. Below is a function that changes the query depending on user input.
func filterQuery(filterText:string) {
let path = Firestore.firestore().collection("all_users").document(uid).collection(filterText).whereField("datefilter", isGreaterThan: self.currentDate)
newSearch(firebasePath: path)
A query is made and a listener is attached on that specific query. This way i can grab the data and listen when something is modified to that specific query.
func newSearch(firebasePath: Query) {
//REMOVE OLD LISTENER IF IT EXISTS OR IF QUERY IS THE SAME ---> ADD NEW ONE AT THE END OF FUNCTION
if self.storedFirebasePath != nil {
if self.storedFirebasePath != firebasePath {
let listener =
self.storedFirebasePath.addSnapshotListener { snapshot, error in
}
listener.remove()
}
}
let queryRef = firebasePath
queryRef.getDocuments() { (querySnapshot, err) in
if err == nil && querySnapshot != nil {
if querySnapshot!.documents.count > 0 {
self.mapIsEmpty = false
for document in querySnapshot!.documents {
let data = document.data()
//MANAGING DATA....
}
} else {
print("NO DATA")
}
}
}
}
//STORE FIRESTORE PATH
self.storedFirebasePath = queryRef
//ATTACH LISTENER
self.realTimeUpdates(firebasePath: firebasePath)
}
}
With code below i get "Currently i get the Error = 'init()' is unavailable: FIRQuery cannot be created directly"
var storedFirebasePath = Query()
Maybe the parameters used to create the query can be stored to recreate the path. Or is there a better practice to do what i'm attempting
I don't think what you're trying to do is possible. You can however store a document reference. But even with a document reference you can't detach listeners with it.
Related
I'm trying to build an iOS app in SwiftUI where users can find a "Post" near to their current location. I have a sub collection called Posts with a geohash. Somewhat annoyingly this library by google has been archived https://github.com/firebase/geofire-objc for no reason. Instead I had to use this library https://github.com/emilioschepis/swift-geohash.
I find all the neighboring geohashes around the current user and then run a query against firstore for each geohash starting with geohash and ending with geohash + '~'.
Here is the function I wrote:
// import https://github.com/emilioschepis/swift-geohash
class FirestorePosts: ObservableObject {
#Published var items = [FirestorePost]() // Reference to our Model
func geoPointQuery(tag:String){
do {
let db = Firestore.firestore().collection("tags")
let docRef = db.document(tag).collection("posts")
// users current location is "gcpu"
let neighbors = try Geohash.neighbors(of: "gcpu", includingCenter: true)
let queries = neighbors.map { bound -> Query in
let end = "\(bound)~"
return docRef
.order(by: "geohash")
.start(at: [bound])
.end(at: [end])
}
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
self.items += documents.compactMap { queryDocumentSnapshot -> FirestorePost? in
return try? queryDocumentSnapshot.data(as: FirestorePost.self)
}
}
for query in queries {
print("ran geo query")
query.getDocuments(completion: getDocumentsCompletion)
}
}
catch{
print(error.localizedDescription)
}
}
}
So far the query works and returns items as expected. However, the results are not updated in realtime when there is a change in Firestore.
How could I make this query update results in realtime? I tried adding query.addSnapshotListener instead but it doesn't like "completion:" parameter
How can I ensure that all the queries are finished before returning the results
You're calling query.getDocuments, which gets data once. If you want to also get updates to that data, you should use addSnapshotListener which listens for updates after getting the initial docs.
To ensure all queries are finished, you could keep a simple counter that you increase each time your addSnapshotListener callback is invoked. When the counter is equal to the number of queries, all of them have gotten a response from the server. That's exactly what the geofire-* libraries for Realtime Database do for their onReady event.
I refactored to this and it seems to work and updates in realtime. I didn't need to use a counter since Im appending the documents to self.items (not sure if thats correct though).
...
for query in queries {
query.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.items += documents.compactMap { queryDocumentSnapshot -> FirestorePost? in
return try? queryDocumentSnapshot.data(as: FirestorePost.self)
}
}
}
I have a repository in my SwiftUI app that grabs all actions for a particular user from Google Cloud Firestore. It uses a Snapshot Listener to keep the action list up to date.
I wanted to change the query to limit results to actions that weren't already completed, by adding this line to my code:
.whereField("completed", isEqualTo: false)
And here is the function it was added to, that creates the listener and sets it to the "actions" variable in the repository:
func loadData() {
let userId = Auth.auth().currentUser?.uid
if userId != nil {
self.listener = db.collection("action")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId!)
.whereField("completed", isEqualTo: false)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.actions = querySnapshot.documents.compactMap { document in
do {
let x = try document.data(as: Action.self)
return x
}
catch {
print(error)
}
return nil
}
}
}
} else {
return
}
}
But once I added that (I have an index for userId, but not for completed, if that matters), it loads data correctly the first time, but the view does not get updated when I add or change existing actions, until I quit and reopen the app.
Is there any issue with having multiple criteria like this for a Snapshot listener? Or anything else that would be causing this listener to not get dynamic updates any more?
You need to create composite indexes for multiple queries
How to create compound queries
You can read the compound queries section of firebase documentation.
Creating compund queries requires some time..
I need to change the whereField variable value in "snapshotListener" Query.
Even if I call the code listener?.remove before the change, snapshot according to the previous query criteria is restarting to listening, while i am calling the "shapshotListener" with new whereField value.
At the same time, snapshot according to the new query criteria is starting to listening also.
So ı am receiving 2 different snapshots when there is any change in the documents.
I would like to explain with code:
Step 1 (think that UserID is 111)
listener = Firestore.firestore().collection("Annotations").whereField("UserID", isEqualTo: \(UserID)).addSnapshotListener { (snapshot, error) in
if error != nil {}else {
if snapshot?.isEmpty == false && snapshot != nil {
for document in snapshot!.documents {
if let snapshotUserID = document.get("UserID") as? String {
print("snapshotUserID: \(snapshotUserID)")
print("UserID: \(UserID)")
}
}
}
}
}
Step 2
listener?.remove
Step 3
UserID = 112
Step 4
listener = Firestore.firestore().collection("Annotations").whereField("UserID", isEqualTo: \(UserID)).addSnapshotListener { (snapshot, error) in
if error != nil {}else {
if snapshot?.isEmpty == false && snapshot != nil {
for document in snapshot!.documents {
if let snapshotUserID = document.get("UserID") as? String {
print("snapshotUserID: \(snapshotUserID)")
print("UserID: \(UserID)")
}
}
}
}
}
Now, I have 2 shapshotListeners for both of UserID documents.
Think that if there will be any change in the document with UserID 111, in the console, it will write like this;
snapshotUserID: 111
UserID: 112
How can I change the query parameter of shapshotListener properly?
It's not possible to change a query while a listener is attached and receiving results. What you're doing right now is the best you can do - remove the listener, build a new query, and add a new listener to that query. If you don't want to handle any duplicated data from the new query, you will to figure out how to skip the snapshots you don't want to process.
My current firebase structure is Collection of Users which then have a subcollection of habits. For a given user, I want them to be able to add to their own collection of routines. however, running into an issue. When I run the function below, it just creates a separate user with a separate routine. How would I tie a new routine to a current authenticated user?
func addData(routineMsg: String){
let db = Firestore.firestore()
let user = db.collection("users").document()
let routine = db.collection("users").document("GzsHAHq1P0uXGdlYwF8P").collection("routines").document()
routine.setData(["id": routine.documentID, "routine": routineMsg]) { err in
if err != nil {
print((err?.localizedDescription)!)
return
}
}
}
Right now, the code shows how I hard-code it to a certain document (GzsHAHq1P0uXGdlYwF8P), but would like to be able to determine the document dynamically by user
let user = db.collection("users").document()
By not passing document() an argument, what you are doing is creating a new document reference with an auto-generated document ID. What you want to do is pass the method with a string that locates the user's document. Ideally, this would be the user's ID:
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let userDocRef = db.collection("users").document(uid)
From there, to generate random document IDs in the subcollection, do what you were doing before:
func addData(routineMsg: String) {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let db = Firestore.firestore()
let userDocRef = db.collection("users").document(uid)
let routineDocRef = userDocRef.collection("routines").document()
routineDocRef.setData([
"id": routineDocRef.documentID,
"routine": routineMsg
]) { error in
if let error = error {
print(error)
}
}
}
I am using Firestore. I am trying to listen for when a new user is added. The problem is, each user also has a friends dictionary. So when I use a snapshot, my code is detecting both events of (1) A new user being added and (2) a new friend being added.
I have tries iterating over the document changes data and restricting doc.document.data()["friends"] == nil. Why isn't this working/how can I properly add a restriction to only include when a new user is added?
func observeUsers(onSuccess: #escaping(UserCompletion)) {
Ref().firestoreUserRef.collection("users").addSnapshotListener { (querySnapshot, error) in
if error != nil {
print("error with observeUser snapshot")
return
}
querySnapshot?.documentChanges.forEach { doc in
//I want to detect that a new user was added, I do not want to detect if a friend was added
if (doc.type == .added) && doc.document.data()["friends"] == nil {
guard let dict = querySnapshot else { return }
for document in dict.documents {
var dictionary = [String : Any]()
dictionary = document.data()
if let user = User.transformUser(dict: dictionary) {
onSuccess(user)
}
}
}
}
}
}
The easiest way is starting from the data model to support the types of queries you want. In this case, when you create a new user (which I assume always has no friends), set the friends field explicitly to null. This will let you query for all new users:
Ref().firestoreUserRef.collection("users").whereField("friends", isEqualTo: NSNull()).addSnapshotListener ...
An assumption here is your transformUser process will update the document and replace null with either a list of friends of an empty array so that it no longer matches the query.