How can I create a snapshot to listen to users being ADDED to my Firestore database? - swift

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.

Related

How to store a full Firestore query path?

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.

Swift / Firestore setData with multiple documents at once causes app to crash

I am creating a fitness app where you can favorite trainer accounts and when you favorite a trainer account I want to add all workouts posted by that trainer to the current user's feed.
When you favorite a trainer account I use this function to add the workouts to a collection on firestore:
func addToUserFeed() {
guard let trainerUid = trainer.id else { return }
guard let currentUid = AuthViewModel.shared.userSession?.uid else { return }
COLLECTION_WORKOUTS.whereField("ownerUid", isEqualTo: trainerUid).addSnapshotListener { snapshot, _ in
guard let workoutIDs = snapshot?.documents.map({ $0.documentID }) else { return }
workoutIDs.forEach { id in
COLLECTION_USERS.document(currentUid).collection("user-feed").document(id).setData([:])
}
}
}
Similarly, when you unfavorite a trainer I am removing those workouts from the user feed collection with this function:
func removeFromUserFeed() {
guard let trainerUid = trainer.id else { return }
guard let currentUid = AuthViewModel.shared.userSession?.uid else { return }
COLLECTION_WORKOUTS.whereField("ownerUid", isEqualTo: trainerUid).addSnapshotListener { snapshot, _ in
guard let workoutIDs = snapshot?.documents.map({ $0.documentID }) else { return }
workoutIDs.forEach { id in
COLLECTION_USERS.document(currentUid).collection("user-feed").document(id).delete()
}
}
}
Then to display these workouts on the feed page view in my app I fetch all the workouts in the user-feed collection on firestore with this function:
//FETCH WORKOUTS SAVED BY THE USER
func fetchFavoriteWorkouts() {
guard let currentUid = AuthViewModel.shared.userSession?.uid else { return }
COLLECTION_USERS.document(currentUid).collection("user-feed").addSnapshotListener { snapshot, _ in
guard let workoutIDs = snapshot?.documents.map({ $0.documentID }) else { return }
//THIS MAY CAUSE AN UNNECCESSARY AMOUNT OF WRITES TO THE APP
self.workouts.removeAll()
workoutIDs.forEach { id in
COLLECTION_WORKOUTS.document(id).addSnapshotListener { snapshot, _ in
guard let workout = try? snapshot?.data(as: Workout.self) else { return }
self.workouts.append(workout)
print("\(workoutIDs)")
}
}
}
}
This is what the firestore collection looks like for user-feed. It adds workout ID documents to the collection that match up with workouts posting by the trainer you just favorited:
Firestore structure
This is working properly in a sense that when you favorite a trainer account it correctly adds multiple documents to the user-feed collection which correspond to workout IDs posted by the trainer you just favorited, however, when you favorite a trainer account the app just closes (not really crashes), and then when you re-open the app the user-feed correctly displays the workouts from trainers you have favorited.
Is there anything in my code that may be causing this random app close when you favorite a trainer?
I understand that this code may not be the most efficient, but I am really not focused on fixing that at the moment, just want to fix the random app close out.
Edit:
The crash only happens when I favorite a trainer that has multiple workouts posted. So I guess something causes the crash when I add multiple documents to a collection at once? Because if I favorite a trainer that only has one workout then it adds that workout to my feed without crashing at all.
the problem is caused by the listener getting called each time a workout is added to workout list and adding duplicated values to the list .
change addSnapshotListener to getDocuments fetchFavoriteWorkouts()
and call this fetch function inside .onAppear() in feed View

How can I reference second level collections in Firebase?

Currently my Firebase database is using the following structure
Users
User 1 Data
User 2 Data...
However I have created a new collection inside of the "users" collection called "products" which will store product details that users have uploaded on the application.
How can I ensure that once a user uploads a new product, it is only uploaded into their 'User X data' dataset, inside the respective "products" collection. The code I currently have only uploads the data into the "users" collection, with no reference of the users who added the product as required. I have shown the structure of my Firebase database below for reference.
Here is my code:
let user = Auth.auth().currentUser
db.collection("users").getDocuments { (snapshot, error) in
if let error = error {
print(error)
return
} else {
for document in snapshot!.documents {
let data = document.data()
let userId = data["uid"] as! String
if userId == user?.uid {
db.collection("users").document("products").setData(["productNameField":firstName, "productURLField":lastName,"productPriceField":ebayName, "productDescriptionField":etsyName, "productTimeRemainingField":email])
}
}
}
}
How would I go about updating my code to achieve this?
I think you're looking for
let user = Auth.auth().currentUser
db.collection("users").getDocuments { (snapshot, error) in
if let error = error {
print(error)
return
} else {
for document in snapshot!.documents {
let data = document.data()
let userId = data["uid"] as! String
if userId == user?.uid {
document.reference.collection("products").addDocument(["productNameField":firstName, "productURLField":lastName,"productPriceField":ebayName, "productDescriptionField":etsyName, "productTimeRemainingField":email])
}
}
}
}
So here document is the DocumentSnapshot that you're looping over, so document.reference gives you the reference to that specific document, and document.reference.collection("products") then points to the `products subcollection for that specific document.
You're wastefully looping over the entire list of users to FIND the user document needed. Simplify, simplify - use the user.uid as the Id of the user document!! (i.e. when you create the user document, save it as
db.collection("users").doc(user.uid).set({whatever})
...then it's trivial to access as...
let user = Auth.auth().currentUser
db
.collection("users")
.doc(user.uid)
.collection("products")
.setData([
"productNameField":firstName,
"productURLField":lastName,
"productPriceField":ebayName,
"productDescriptionField":etsyName,
"productTimeRemainingField":email
]);
If there is another reason to keep the docID and the uid separate (not shown here), then use a query to get the SPECIFIC document with the uid, rather than downloading ALL of them
let user = Auth.auth().currentUser
db
.collection("users")
.where(field: "uid", opStr:"==", value: user.uid)
.getDocuments( { (snapshot, error) in
if let error = error {
print(error)
return
} else { //the query should only return the one document, but queries always return an array
snapshot
.documents[0]
.ref()
.document("products")
.setData([
"productNameField":firstName,
"productURLField":lastName,
"productPriceField":ebayName,
"productDescriptionField":etsyName,
"productTimeRemainingField":email
])
}
}
...etc...
I don't generally use Swift, so the where clause may require different formatting, but the idea is GET ONLY THE DOCUMENT YOU NEED. Your security rules should only be allowing this, anyway.

How can i change the whereField value of shapshotListener properly in swift?

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.

Function is not waiting to adjust boolean values (by checking a field in Firestore) and returns default values before

I'm trying to return a list of booleans to check in what registration step the user is and if it has registered through Facebook or Phone. The function I made gets the user doc and checks for two fields to see if they exist (interest and id). id, because only FB users have that field and interests because that is the last step of the registration process. It appears the function is returning the default values [true,true] before I even get the chance to modify the values.
fileprivate func checkRegistrationComplete(_ uid: String?) -> [Bool] {
//I will return a list with the first bool to know if it has filled all the info and the second if it has done so through FB or phone
var userRegistered = true
var userRegisteredFB = true
let db = Firestore.firestore()
let docRef = db.collection("users").document(uid!)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data()
userRegistered = dataDescription?["interest"] != nil
userRegisteredFB = dataDescription?["id"] != nil
print(userRegisteredFB)
print(userRegistered)
} else {
print("Document does not exist")
}
}
print(userRegisteredFB,userRegistered)
return [userRegistered,userRegisteredFB]
}
The output should be [false,true] but it returns [true,true]. And the print functions show that the get method is actually working but it shows after the return call