Updating Firebase Data for Any User in Swift - 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
Related
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
FireStore - how to get around array "does-not-contain" queries
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.
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 )}
How to query nested data in Firebase Database?
How can I make a query to get all bookings of all users that have timestamp in the range of 1519912278 ...1520689878 ? In Firebase docs it states that I can query deeply nested children. According to this answer, it should work https://stackoverflow.com/a/47805607 , but it return null. What is the flow of my User app? : 1. User singn up 2. When booking is made, it is saved at /Users/UID/stripe_customer_created_with_card/bookingNumber{booking object} Why do I need all bookings located under /Users?: There is a second app designed for Workers which can observe /Users node and each worker should be able to get bookings from /Users based on a timeStamp. For example a worker1382 can read bookings made by /Users from 16 March 2018 to 24 March 2018. dbRef = FIRDatabase.database().reference().child("Users") dbRef.queryOrdered(byChild: "TimeStampDateAndTime") .queryStarting(atValue: "1519912278") .queryEnding(atValue: "1520689878") .observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in print(snapshot.value) //returns null } Users UID cus_CRC50bSq7rXuHv //stripe customer id for the user 617643762 //booking number TimeStampDateAndTime: "1521309610" Postcode: "E14 9DS" BookingNumber:"617643762" Another question: The parent key of the booking object is equal to BookingNumber. If I replace the parent key 617643762 with TimeStampDateAndTime value 1521309610, would I be able to order the bookings by time stamp and ultimately get only the ones that are within a certain range 1519912278 ...1520689878? So, the path would look like Users/UID/cus/1521309610: {object goes here} From Firebase Documentation Queries can also be ordered by deeply nested children, rather than only children one level down. This is useful if you have deeply nested data like this: { "lambeosaurus": { "dimensions": { "height" : 2.1, "length" : 12.5, "weight": 5000 } }, } To query the height now, we use the full path to the object rather than a single key. Now, my question is, what can't I do if I don't know the full path? let ref = Firebase(url:"https://dinosaur-facts.firebaseio.com/dinosaurs") ref.queryOrderedByChild("dimensions/height").observeEventType(.ChildAdded, withBlock: { snapshot in if let height = snapshot.value["height"] as? Double { println("\(snapshot.key) was \(height) meters tall") } }) EDIT 2 Initial Structure looks like this: { "Users" : { "I50dHN7wPqPJClPfYDG76gXizdD2" : { //-> user?.uid of a logged in user "cus_CRC50bSq7rXuHv" : { "617643762" : { "TimeStampDateAndTime" : "1521309610" } } } } } Modified structure: { "Users" : { "I50dHN7wPqPJClPfYDG76gXizdD2" : { "1521309610" : { // -> store timeStamp as the immediate child key of UID rather than the booking number "TimeStampDateAndTime" : "1521309610", "BookingNumber": "617643762" } } } } { "UsersProfile":{ "I50dHN7wPqPJClPfYDG76gXizdD2":{ "StripeCustomer":"cus_CRC50bSq7rXuHv", // -> store StripeCustomer on the UsersProfile "Name": "John Doe" } } }
You can query properties at a known path under each child node. So you can query dimensions/weight in the dinosaur sample, and you can query 617643762/TimeStampDateAndTime from your data. But you can't query */TimeStampDateAndTime, since that's not a known path. If you need to get a list of bookings in a date range across all users, you will need to modify your data structure to allow that query. For example, by adding a top-level list of bookings: "Bookings": { "book_617643762" Customer: "cus_CRC50bSq7rXuHv" TimeStampDateAndTime: "1521309610" Postcode: "E14 9DS" } With this additional structure, you can then query: dbRef = FIRDatabase.database().reference().child("Bookings") dbRef.queryOrdered(byChild: "TimeStampDateAndTime") .queryStarting(atValue: "1519912278") .queryEnding(atValue: "1520689878") .observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in print(snapshot.value) //returns null }