FireStore - how to get around array "does-not-contain" queries - swift

After some research, it's seems clear that I cannot use FireStore to query items a given array does NOT contain. Does anyone have a workaround for this use case?...
After a user signs up, the app fetches a bunch of cards that each have a corresponding "card" document in FireStore. After a user interacts with a card, the card document adds the user's uid to a field array (ex: usersWhoHaveSeenThisCard: [userUID]) and the "user" document adds the card's uid to a field array (ex: cardsThisUserHasSeen: [cardUID]). The "user" documents live in a "user" collection and the "card" documents live in a "card" collection.
Currently, I'd like to fetch all cards that a user has NOT interacted with. However, this is problematic, as I only know the cards that a user has interacted with, so a .whereField(usersWhoHaveSeenThisCard, arrayContains: currentUserUID) will not work, as I'd need an "arrayDoesNotContain" statement, which does not exist.
Finally, a user cannot own a card, so I cannot create a true / false boolian field in the card document (ex: userHasSeenThisCard: false) and search on that criteria.
The only solution I can think of, would be to create a new field array on the card document that includes every user who has NOT seen a card (ex: usersWhoHaveNotSeenThisCard: [userUID]), but that means that every user who signs up would have to write their uid to 1000+ card documents, which would eat up my data.
I might just be out of luck, but am hoping someone more knowledgeable with NOSQL / FireStore could provide some insight.
// If any code sample would help, please let me know and I'll update - I think this is largely conceptual as of now

As you've discovered from query limitations, there is no easy workaround for this using Cloud Firestore alone. You will need to somehow store a list of documents seen, load that into memory in the client app, then manually subtract those documents from the query results of all potential documents.
You might want to consider augmenting your app with another database that can do this sort of operation more cleanly (such as a SQL database that can perform joins and subqueries), and maintain them in parallel.
Either that, or require all the documents to be seen in a predictable order, such as by timestamp. Then all you have to store is the timestamp of the last document seen, and use that to filter the results.

There is an accepted and good answer, however, it doesn't provide a direct solution to the question so here goes... (this may or may not be helpful but it does work)
I don't know exactly what your Firestore structure is so here's my assumption:
cards
card_id_0
usersWhoHaveSeenThisCard
0: uid_0
1: uid_1
2: uid_2
card_id_1
usersWhoHaveSeenThisCard
0: uid_2
1: uid_3
card_id_2
usersWhoHaveSeenThisCard
0: uid_1
1: uid_3
Suppose we want to know which cards uid_2 has not seen - which in this case is card_id_2
func findCardsUserHasNotSeen(uidToCheck: String, completion: #escaping ( ([String]) -> Void ) ) {
let ref = self.db.collection("cards")
ref.getDocuments(completion: { snapshot, err in
if let err = err {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else {
print("no docs")
return
}
var documentsIdsThatDoNotContainThisUser = [String]()
for doc in docs {
let uidArray = doc.get("usersWhoHaveSeenThisCard") as! [String]
let x = uidArray.contains(uidToCheck)
if x == false {
documentsIdsThatDoNotContainThisUser.append(doc.documentID)
}
}
completion(documentsIdsThatDoNotContainThisUser)
})
}
Then, the use case like this
func checkUserAction() {
let uid = "uid_2" //the user id to check
self.findCardsUserHasNotSeen(uidToCheck: uid, completion: { result in
if result.count == 0 {
print("user: \(uid) has seen all cards")
return
}
for docId in result {
print("user: \(uid) has not seen: \(docId)")
}
})
}
and the output
user: uid_2 has not seen: card_id_2
This code goes through the documents, gets the array of uid's stored within each documents usersWhoHaveSeenThisCard node and determines if the uid is in the array. If not, it adds that documentID to the documentsIdsThatDoNotContainThisUser array. Once all docs have been checked, the array of documentID's that do not contain the user id is returned.
Knowing how fast Firestore is, I ran the code against a large dataset and the results were returned very quickly so it should not cause any kind of lag for most use cases.

Related

Updating Firebase Data for Any User in Swift

I am trying to allow documents (reports) that are created by a user to be viewed by other users and then updated with changes. The documents show up in a tableView that when selected show the appropriate data for that specific report loaded up in a ViewController. However when I go to select the update button within that ViewController of the individual report, the error comes back that no document can be found.
Below is the current variation of code I am working with for the update function.
private let database = Firestore.firestore()
private init() {}
// UPDATE REPORT
public func updateTheData(
reportPost: ReportPost,
thisReport: String,
completion: #escaping (Bool) -> Void
){
let data = [
"id": reportPost.identifier,
"title": reportPost.title,
"timestamp1": reportPost.timestamp1,
"address": reportPost.address,
"customerPO": reportPost.customerPO,
"authNum": reportPost.authNum,
"contactName": reportPost.contactName,
"contactPhone": reportPost.contactPhone,
"modelNum": reportPost.modelNum,
"serialNum": reportPost.serialNum,
"addInfo": reportPost.addInfo,
"equipProblem": reportPost.equipProblem,
"action": reportPost.action,
"followUp": reportPost.followUp,
"techName1": reportPost.techName1,
"techName2": reportPost.techName2,
"techName3": reportPost.techName3,
"techName4": reportPost.techName4,
"timestamp2": reportPost.timestamp2,
"timestamp3": reportPost.timestamp3,
"timestamp4": reportPost.timestamp4,
"milesTraveled1": reportPost.milesTraveled1,
"milesTraveled2": reportPost.milesTraveled2,
"milesTraveled3": reportPost.milesTraveled3,
"milesTraveled4": reportPost.milesTraveled4,
"travelTime1": reportPost.travelTime1,
"travelTime2": reportPost.travelTime2,
"travelTime3": reportPost.travelTime3,
"travelTime4": reportPost.travelTime4,
"siteTime1": reportPost.siteTime1,
"siteTime2": reportPost.siteTime2,
"siteTime3": reportPost.siteTime3,
"siteTime4": reportPost.siteTime4,
"serviceType1": reportPost.serviceType1,
"serviceType2": reportPost.serviceType2,
"serviceType3": reportPost.serviceType3,
"serviceType4": reportPost.serviceType4,
"timeTotal1": reportPost.timeTotal1,
"timeTotal2": reportPost.timeTotal2,
"timeTotal3": reportPost.timeTotal3,
"timeTotal4": reportPost.timeTotal4,
"partNum1": reportPost.partNum1,
"partNum2": reportPost.partNum2,
"partNum3": reportPost.partNum3,
"partNum4": reportPost.partNum4,
"quantity1": reportPost.quantity1,
"quantity2": reportPost.quantity2,
"quantity3": reportPost.quantity3,
"quantity4": reportPost.quantity4,
"price1": reportPost.price1,
"price2": reportPost.price2,
"price3": reportPost.price3,
"price4": reportPost.price4,
"priceTotal1": reportPost.priceTotal1,
"priceTotal2": reportPost.priceTotal2,
"priceTotal3": reportPost.priceTotal3,
"priceTotal4": reportPost.priceTotal4,
]
let docRef = database.collection("reports").document(thisReport)
docRef.updateData(data) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
}
For Reference, this is how a report is saved and uploaded to the database in Firebase.
// FSR REPORT POSTING
public func insert(
reportPost: ReportPost,
email: String,
completion: #escaping (Bool) -> Void
) {
let userEmail = email
.replacingOccurrences(of: ".", with: "_")
.replacingOccurrences(of: "#", with: "_")
let data = [
"id": reportPost.identifier,
"title": reportPost.title,
"timestamp1": reportPost.timestamp1,
"address": reportPost.address,
"customerPO": reportPost.customerPO,
"authNum": reportPost.authNum,
"contactName": reportPost.contactName,
"contactPhone": reportPost.contactPhone,
"modelNum": reportPost.modelNum,
"serialNum": reportPost.serialNum,
"addInfo": reportPost.addInfo,
"equipProblem": reportPost.equipProblem,
"action": reportPost.action,
"followUp": reportPost.followUp,
"techName1": reportPost.techName1,
"techName2": reportPost.techName2,
"techName3": reportPost.techName3,
"techName4": reportPost.techName4,
"timestamp2": reportPost.timestamp2,
"timestamp3": reportPost.timestamp3,
"timestamp4": reportPost.timestamp4,
"milesTraveled1": reportPost.milesTraveled1,
"milesTraveled2": reportPost.milesTraveled2,
"milesTraveled3": reportPost.milesTraveled3,
"milesTraveled4": reportPost.milesTraveled4,
"travelTime1": reportPost.travelTime1,
"travelTime2": reportPost.travelTime2,
"travelTime3": reportPost.travelTime3,
"travelTime4": reportPost.travelTime4,
"siteTime1": reportPost.siteTime1,
"siteTime2": reportPost.siteTime2,
"siteTime3": reportPost.siteTime3,
"siteTime4": reportPost.siteTime4,
"serviceType1": reportPost.serviceType1,
"serviceType2": reportPost.serviceType2,
"serviceType3": reportPost.serviceType3,
"serviceType4": reportPost.serviceType4,
"timeTotal1": reportPost.timeTotal1,
"timeTotal2": reportPost.timeTotal2,
"timeTotal3": reportPost.timeTotal3,
"timeTotal4": reportPost.timeTotal4,
"partNum1": reportPost.partNum1,
"partNum2": reportPost.partNum2,
"partNum3": reportPost.partNum3,
"partNum4": reportPost.partNum4,
"quantity1": reportPost.quantity1,
"quantity2": reportPost.quantity2,
"quantity3": reportPost.quantity3,
"quantity4": reportPost.quantity4,
"price1": reportPost.price1,
"price2": reportPost.price2,
"price3": reportPost.price3,
"price4": reportPost.price4,
"priceTotal1": reportPost.priceTotal1,
"priceTotal2": reportPost.priceTotal2,
"priceTotal3": reportPost.priceTotal3,
"priceTotal4": reportPost.priceTotal4,
]
database
.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.setData(data) { error in
completion(error == nil)
}
}
When the report is first inserted, it's stored at this path
database.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.setData(data)
which is
users/the_email/reports/some id
but then when you're attempting to update that report it's at this path
database.collection("reports")
.document(thisReport)
.updateData(data)
which is
database/reports/some id
and that's two different locations.
To update data, it needs to point to the same document reference. so the data needs to be updated at the same location it was initially written to
database.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.updateData(data) //<- same path as it was initially
As a side note, the documentId is both being stored as the documentId as well as within the document in the "id" field and there's no reason to do that. A documentId is static and if it's known, you can get to that data directly without a query.
I would also suggest not using users emails as documentIds'; user emails can change and if that happens you won't be able to find the report. Additionally, if you want to update that, you'll have to find every occurance of that old email in the entire database, read it and it's child data in, delete it and re-write it. What a pain.
Also, substituting _ for # and . in the email address may work to a point but if want to get back that email address, there could be multiple _ in the address; how would you know which _ was the # and which one was actually part of the email address.
some_person#thing.com -> some_person_thing_com = some#person_thing.com?
EDIT
Per the question, suppose there are a series of reports that are 1) shared amongst users 2) the creating user can edit their own reports
Here's one option for storing the reports
reports (collection)
document_0 //a document with an auto-generated documentId
created_by_uid: "uid_0"
report_info: "some info about this report, fields etc"
document_1
created_by_uid: "uid_1"
report_info: "some info about this report"
The reports collection can be read by any user, which meets criteria 1. It's easy to get the reports created by user with uid_0 as a simple query can be run against the reports collection where created_by_uid = uid_0.
Here's the code with an object to hold the report data: documentId the report_info (the report data) and a field to track who created the report. Suppose user with uid_0 is logged in and wants to edit a report. We'll load all of the reports uid_0 created and store them in an array:
class ReportClass {
var id = ""
var report_info = ""
var created_by_uid = ""
}
var reportArray = [ReportClass]()
func readReports(forUid: String) { //pass in uid_0
let reportsCollection = self.db.collection("reports")
reportsCollection.whereField("created_by_uid", isEqualTo: forUid)
reportsCollection.getDocuments(completion: {documentSnapshot, error in
for doc in documentSnapshot!.documents {
//note we don't need the created_by_uid because it's not going to change
let report = ReportClass()
//keep track of the document Id
report.id = doc.documentID as! String
//the report info which the user is changing
report.report_info = doc.get("report_info") as! String
self.reportArray.append(report)
}
})
}
now the reports are presented to the user so they can select one and edit it. Upon saving we update the ReportClass object report_info field with the new data the user input and pass the object to a function to update that report
func updateReport(theReport: ReportClass) {
let reportsCollection = self.db.collection("reports")
let thisReport = reportsCollection.document(theReport.id)
//the report document path is reports/documentId
thisReport.updateData(["report_info": theReport.info]) //only update the report_info field
}
The key here is we load in and store the users reports, keeping the report documentId in the object. That keeps track of the path to the report
reports/report id/id, report_info and created_by_uid fields
then when the report is ready to be written, we derive the path from the documentId stored in the object so the path is the same.
I did a lot of this long-hand, but you should be using Codable objects as it makes populating them a snap
See Mapping Firestore Data in Swift

How to display specific data, and group it together in firestore

I'm extremely new to firebase and need to display all the data in my collection. Within my app there is an integrated quiz function, and when the 'submit score' button is pressed, the data is sent to Firestore as a new document based on the uid.
user collection in firebase, new document based on uid .
This is what I have so far:
func getData() {
let db = Firestore.firestore()
// Get data
db.collection("users").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
for document in querySnapshot!.documents {
self.studentlbl.text = ("\(document.documentID) => \(document.data())");
}
}
}
This displays the following: result
I'm trying to figure out how to display the first name, followed by the user's corresponding score.
Thanks
You can display the specific field by adding field name in document.data() or doc.data() or the example below:
document.data().firstname;
or in your case(swift) if I'm correct:
self.studentlbl.text = ("(document.data().firstname");
Regarding to the score of the users, I'll recommend creating a new collection to store the data of quiz scores for every users. You can use this answer for user and post as the reference and example that can help you how you can build the database structure of your application. The answer also include how you will query or group it together.

If you have a firebase snapshot with a filter applied to the current user’s child, how can one do that for all users in the database?

If you have a firebase snapshot that you are filtering for the current user, how do you do that for all users in the database?
Path: (people.child(uid).child(peopleWhoLike2)
It works for the current user, but I want it for all users. Probably something like for peopleArray in snapshot.children.allObjects as! [DataSnapshot] { needs to be used. Below is code for current
let thisUserRef1 = Database.database().reference().child("people").child(uid)
let myPeopleRef1 = thisUserRef1.child("peopleWhoLike2")
myPeopleRef1.observeSingleEvent(of:DataEventType.value, with: { snapshot in
print("klk")
let peopleArray = snapshot.children.allObjects as! [DataSnapshot]
let filteredResults = peopleArray.filter { person in
let personUid = person.value as! Int
let coordSnap12 = personUid
print("kjk", coordSnap12 )
let date = Date(timeIntervalSince1970: TimeInterval(coordSnap12)/1000.0)
//let secondsInDay = 86400
return Calendar.current.isDateInToday(date)
}
print(filteredResults, "ppp")
let countb = filteredResults.compactMap({$0}).count
print(countb, "ccc")
JSON DB Structure
"people" : {
"1ZWT7FAE2qThNQfBj7tbMO7BnMo1" : {
"Coordinates" : {
"latitude" : 50.054738,
"longitude" : 8.226809826085624
"peopleWhoLike2" : {
"1vLVFwrXrHUoakmDrnQKwbv08Yj1" : 1581548952597,
"F9NX0UCG4fVHCKFk2VZ1NZKsLro2" : 1586210112155,
"IrrBgFY9C1ekMmHUkQRzc5LhbDu1" : 1581547417432,
Firebase Realtime Database doesn't have a direct way to get the count of number of children in a node without reading in that nodes data. So there are two methods.
Observe the node, iterating over the children with .childAdded to get a count. Or, along those same lines load in all of the children with .value - to which you can get a count of the snapshots children. The downside there is it could be a LOT of data so with larger datasets, or unknown amount of children this is discouraged. It's also very read heavy obviously as if you have 1000 users, it's a lot of data to parse through.
Keep a separate node of the child count (this is the winner!)
The additional node would look like this
like_me_counts
"1ZWT7FAE2qThNQfBj7tbMO7BnMo1": 3
"another user id": 20
"yet another user id": 12
So as a user has another person like them, add the 'liker' to the "peopleWhoLike2" node as shown in your question, and then increment that users matching node within the like_me_counts node.
In this case if another person like user 1ZWT7... read that in from the like_me_counts node to get the value of 3, add one and then write out 4. If a user unlikes them, read it in, subtract 1 and write out the new value.
You can wrap all of that into a transaction so it either all succeeds or all fails.
EDIT
I would even go so far to say that it may be best to break out the people who like you into it's own node
like_me
uid_0
user_10: true
user_11: true
uid_1
user_39: true
uid_2
user_46: true
What that structure, each user would add a child added observer to their like_me node, and when someone likes them... say user_52 likes user_2
uid_2
user_46: true
user_52: true
then uid_2 will receive that notification and can handle it. Obviously, I am just using true as a placeholder but there could be other info such as a time stamp or even child nodes that can be used for filtering (for todays likes only for example)
uid_2
user_46: 20200624
user_52: 20200629

Check if a new field was added in a specific document like documentChanges for a collection in Firestore

I use this code for load comments in a table view:
func observePostComments(postId: String, completion: #escaping (String) -> Void) {
let db = Firestore.firestore()
db.collection("post-comments").document(postId).addSnapshotListener { (snapshot, err) in
if snapshot!.exists {
for key in (snapshot?.data()!.keys)! {
completion(key)
}
} else {
return
}
}
}
It works like it should, but every time a user creates a new comment, all comments are added again. I know how it works for a collection with:
querySnapshot?.documentChanges.forEach { diff in
if (diff.type == .added) { ....
But I can not figure out how to implement that functionality on a document / field level. If I want to do the same on a document level, I receive
Value of type 'DocumentSnapshot?' has no member 'documentChanges'.
How can I track changes on a specific document level, when a new Key-Value pair was added to a document?
Firestore's change detection only works on complete documents. If you need to know what changed inside a document, you will have to detect this in your own code, for example by comparing the previous DocumentSnapshot with the new one.
The exact way to do this depends a bit on what data you store, but there are two broad approaches:
You take something that is unique about each comment, and check if that's already present in your UI. This can for example be the ID of each comment, but anything else that's unique works too.
You store a timestamp for each comment, and keep track of the most recent timestamp you've already processed. Then in an update, you skip all comments up until that timestamp.
Another approach would be to clear the UI before adding the same comments to it. So something like:
db.collection("post-comments").document(postId).addSnapshotListener { (snapshot, err) in
if snapshot!.exists {
clearCommentsFromUI() // this is a function you will have to implement
for key in (snapshot?.data()!.keys)! {
completion(key)
}

How to store data in Firestore (Swift)

I have an iOS app using Cloud Firestore and have problems with updating the data. My goal is to add urls to a dictionary one by one, but all I get is rewritten one value. How should I use setData and updateData? Tried it different ways
storageRef.child("users/" + currentUser.value!.documentID + "/" + faceRef.documentID + ".jpg")
.putData(data!).observe(.success) { (snapshot) in
guard let downloadURL = snapshot.metadata?.downloadURL()?.absoluteString else { return }
let db = self.fsReference.document(self.currentUser.value!.documentID)
var dict = ["faces": ["": ""]]
dict["faces"] = ["newvalue\(downloadURL.hashValue)": downloadURL]
db.updateData(dict)
completion?()
Here's what I tried. Any advice would be nice, thanks in advance!
UPD: Tried to move my dictionary to subcollection, but after .collection("newCollection").document("newdocument") collection does not appear. What might be the problem?
So what I am seeing is you are using cloud storage to save profile pictures and you want to save each one of the urls those pictures. You need to understand that both setValue() and updateValue() do just about the same thing. A note with updateValue() is it will create that document if it doesn't already exist. So, when updating values in Firestore understand that it sets the value to what you give it, which can be misleading at first.
1st When updating any document start by getting the document first. If people are constantly updating different document you may want to consider using Firestore transactions: https://firebase.google.com/docs/firestore/manage-data/transactions#transactions
This will make sure that your data is updated correctly.
2nd Append the URL to the to the array, I am not how you set it up, but I would setup the firestore to look something like this
"users" = [
"unique_id = "{
"firstname": "John",
"lastname": "Doe",
"unique_id": "document_id_here"
"faces": [ {key: value} ]
}
]
When you serialize that object your faces object should be this [[String: Any]]
3rd, last step would be to get the document and update just that value
// Get the value in the completion with the data use this code
// Drill down to the property you want to update using the completion data ex.
var faces = completedData.faces
faces.append("[key: value]")
// Update the data back to firestore
let path = Firestore.firestore().collection("users").document("unique_user_id")
// Merging is so important. otherwise it will override your document
path.setData(["facesKey: faces"], merge: true) {(error in
if let error = error {
// good error handling here
}
// Successfully updated document
)}