How to set more than one value to a child in Firebase using Swift? - swift

I am trying to make a money related app which shows the user their spendings as a project to get used to Firebase. So I stumbled upon this issue; I can't seem to figure out how to add more than one expense assigned to a user. Whenever I add a new expense, the existing value in Firebase gets reset to the new value but I want it to store both the new and the old value. How can I do this?

There's something called "autoID" if that helps. I'll link that in a few moments.
Edit: It's used here.

childByAutoId() is what you want for swift. See Updating Or Deleting specific data section in the Read and Write Data on iOS
let thisUserRef = ref.child("users").child(users uid)
let expenseRef = thisUserRef.child("expenses").childByAutoId()
let dict = ["expense_type": "travel", "expense_amt": "1.99"]
expenseRef.setValue(dict)
will results in
users
uid_0
-Y88jn90skda //<- the node key created with childByAutoId
expense_type: "travel"
expense_amt: "1.99"
the childByAutoId is super powerful and allows 'random' node keys to be generated that contain your child data.
See the answer to this question and this other question for some tips on data modeling and queries.

Related

CKModifyRecordsOperation - create referenced records simultaneously?

I'm working to refactor a somewhat clunky iterative save loop for CloudKit to use CKModifyRecordsOperation and bulk save records.
I have a Course, which has 1+ Weeks, each which has 1+ Lessons. Previously I'd create the Course in CloudKit, then create the Weeks, then the Lessons and circle back to update the Weeks with the Lesson references once created. And also fetch and save the Course record with the references to the Weeks once Weeks were created.
I've refactored to create all (Course, Week and Lesson) records locally, with the relevant references set up. E.g., course["weeks"] contains the record references for each week I've created locally, for example:
course["weeks"] = getWeekRefsForCourse(for: allWeeks)
func getWeekRefsForCourse(for allWeeks: [CKRecord]) -> [CKRecord.Reference] {
var weekRefsArray: [CKRecord.Reference] = []
for each in allWeeks {
let weekRef = CKRecord.Reference(record: each, action: .deleteSelf)
weekRefsArray.append(weekRef)
return weekRefsArray
}
The issue is when I go to save, the error I get back is:
Invalid list of records: Cycle detected in record graph
This suggests that I've got a record referring to itself, but I've gone record by record and I I can't see anything. The Weeks reference the Course and the Lessons, but not themselves, etc. So my only theory is that because I'm trying to save items that refer to other items that haven't yet been created, what I'm trying to do isn't possible.
Is the correct protocol here actually my original approach? Or is there something I'm missing?
Original approach:
Save Course
Save Week
Save Lessons
Update Weeks with Lesson references
Update Course with Week references
CKModifyRecordsOperation code:
let bulkSaveQueryOp = CKModifyRecordsOperation()
bulkSaveQueryOp.recordsToSave = [courseRecord]
bulkSaveQueryOp.recordsToSave?.append(contentsOf: weeks)
bulkSaveQueryOp.recordsToSave?.append(contentsOf: lessons)
//note I've confirmed I have the correct number of records
bulkSaveQueryOp.modifyRecordsCompletionBlock = { records, recordIDs, error in
if let error = error as? CKError {
log.error(error)
} else { // success }
}
CKContainer.default().publicCloudDatabase.add(bulkSaveQueryOp)
I'm fairly certain you can create all these records together. I suspect there must be something else wrong and that's why you are getting that error.
You can create a CKReference to an object that doesn't even exist and CloudKit will still create it. A CKReference is little more than a pointer to a recordName of another object in the container.
Combining all those records into a CKModifyRecordsOperation is the right thing to do and you shouldn't have to be careful about the order of your CKRecord saves. I think another issue must be lurking somewhere.
I was able to find a similar question over in the apple dev forums for someone that had a similar issue and and it helped identify the source of the problem - that I had effectively created a loop with my .deleteSelf actions I was creating on various references.
So while the references were fine, the actions were what was causing the error.
Once I double checked and adjusted those, the error went away and I was able to save.
Spotting this was made more complex by the fact that I wasn't changing the .deleteSelf actions version what I had previously done - and was working fine - when my saves were done in sequence rather than a bulk CKModifyRecordsOperation save.
So it seams that another added benefit of CKModifyRecordsOperation with a bulk save is that it adds a layer of stupidity checking that creating items individually doesn't :)

Swift Firestore Generate Document ID before creating a document

I am trying to generate a document id to use on two documents in seperate collections.
I am able to achieve this in an Angular PWA, but I'm now working it into an iOS app.
Basically an appointment document is added to a user, then a top level document is created for ease of display and notifications using the same document id
users/uid/appointments/newID
appointments/uid/appointment/newID
All of this takes place in the same ViewController, so in the code I need to generate a temporary document id to use as the id in both collections.
I hope that makes sense....
would let tempId = db.document().documentID do the trick?
I managed to suss it all out.
After creating my collection reference, let's call it myRef, I did the following:
myRef = db.collection("collection_name")
secondRef = db.collection("second_collection")
let tempId = myRef.document().documentid
myRef.document(tempId).setData(myDictionary)
secondRef.document(tempId).setData(otherDictionary)
That helped create the duplicate document id's i needed.
Hope this helps with someone else...
Cheers
Michael

Deleting parent node with child value

I am struggling to overcome to issue I have. I am trying to delete a parent node/key given I find the correct child value.
My database is structure liked this
I am querying my database by a certain value, objectID, as it will match the postID which is passed through the parameters. The objectID value is removed. However, I am struggling to remove the key in which it falls under.
I have had mixed results so far:
I can either remove the objectID value using this code:
refSnap?.ref.child("objectID").child(postID).removeValue()
Of I can remove the whole notifications node/directory, using this:
refSnap?.ref.child("objectID").queryEqual(toValue: postID).ref.removeValue()
refSnap?.key gives me all the keys/nodes under the notifications node.
I cannot access the key which the objectID and all other information is stored under as it is .childByAutoId. Can anyone possibly help me as to how I can sort this issue?
While the other answer provides some insight with an alternate and workable structure, the ability to delete a node based on a child is fairly straightforward and directly addresses the question without changing the structure.
(note that the structure may need to be changed anyway but for this exercise, we'll use it as is.)
Given a structure suggested in the question:
notification
"rtupy..." //childByAutoId
"-LFEMjAcny..." // childByAutoId
"-LFEzrrq..." // childByAutoId
from: "aw,sdasdad"
objectID: "-LFEMjAcn...."
timestamp: 15292
type: "comment"
Suppose you want to delete the node "-LFEzrrq..." as shown in your screenshot. That node contains the child objectID: "-LFEMjAcn...."
To delete the node you need query for the node that contains the objectID you want which, according to the question, is working and returning the correct child.
Use the returned snapshot to obtain it's parent key, and get the path to that node and delete it. Note that we don't know what process or code the OP used to obtain the node they want to delete - perhaps it was from another query and the node reference was passed in or some other means.
let queryRef = //unknown how, but build the query for objectID = "-LFEMjAcny...."
queryRef.observeSingleEvent(of: .value, with: { snapshot in
let key = snapshot.key //this is the parent key of the objectID node i.e. -LFEMzrrq..."
let parentRef = snapshot.ref.parent! //this is the path to that parent
let refToDelete = parentRef.child(key) //add the parent key to the path: -LFEzrrq
refToDelete.removeValue() //delete it
})
As you can see, regardless of the parent nodes' key, or how deep it is, this code will delete the node found in the query.
The key names do not matter so using .childByAutoId as references to tie your nodes together is safe and generally best practice as disassociating node keys from the data they contain makes your structure highly expandable.
Your issue is that you are using childByAutoId and you can't "tie" these random alphanumerics to something(in this case the post).
The structure that i would set is this:
-notifications
--uid //notifications for user
--- userAid+userBid //follow notification, if they unfollow you already now which one it is and you can go and delete it
---commentNotificationID // you give this notification the same Id that the comment has, so if the user deletes the comment you use that id to delete the notification as well.

Using childByAutoId On Single Value?

I am pretty new to both Swift and Firebase, and I am attempting to make a simple app using Firebase as the backend. As far as I know, there is no memory-efficient way to use the numChildren() function without loading every single child into memory for counting, so I am implementing my own simple counter for the number of "Events" that have been created in my app.
The documentation for Firebase states that the childByAutoID() method should be used for updating lists in multi-user applications. I am assuming it adds a timestamp to the requested update and does them in order.
My question is whether it is necessary to use childByAutoID() when only updating a SINGLE field in a multi-user application. That is, will there be conflicts on my numEvents field if I do:
dbRef = FIRDatabase.database().reference()
dbRef.child("numEvents").setValue(num)
Or must I do:
dbRef = FIRDatabase.database().reference()
dbRef.child("numEvents").childByAutoId().setValue(num)
In order to avoid write conflicts? My only real confusion is that the documentation for childByAutoID stresses that it is useful when the children are a list of items, but mine is only a single item.
If you are only updating a single field you should not be using childByAutoId. To update a child value for an object, you need to obtain a reference to that object somehow, perhaps by a query of some sort (in many cases you will naturally already have a reference to the object if it needs to be changed) and you can change the value like this:
dbRef.child("events").child(objectToUpdateId).child(fieldToUpdateKey).setValue(newValue)
childByAutoId in this context would be used to create a new field like:
dbRef.child("events").childByAutoId().setValue(newObject)
I'm not exactly sure how this applies to your situation, but those are some descriptions of how to update a field, and use childByAutoId.
What childByAutoId does is create a unique key for a node, to avoid using the same key multiple times and then creating data conflicts like inconsistency (not write conflicts) to avoid write conflicts you use the transaction blocks.
The best way to learn is to try it out
If num == 1 , in the first example the result will be
dbRef:{
numEvents:1
}
While the second will be
dbRef:{
numEvents:{
//The auto-generated key
KLBHJBjhbjJBJHB:1
}
}
The childByAutoId would be useful if you want to save in a node multiple children of the same type, that way each children will have its own unique identifier
For example
pet:{
KJHBJJHB:{
name:fluffy,
owner:John Smith,
},
KhBHJBJjJ:{
name:fluffy,
owner:Jane Foster,
}
}
This way you have a unique identifier for cases where there is no clear way with the item data to guarantee it will be unique (in this case the pet's name)
Few things here:
childByAutoId is not a timestamp. But is used to create unique nodes in any given node.
Use case of childByAutoId :
You have messages node which stores messages from multiple user who are involved in a group chat. So each user can add messages in the group chat so you would do something like this each time user sends message:
dbRef = FIRDatabase.database().reference()
dbRef.child("messages").childByAutoId().setValue(messageText)
So this will create a unique message id for each message from different users. This will kind of act like primary key of message in normal databases.
The structure of database will be something like this:
messages: {
"randomIdGenerated-12asd12" : "hello",
"randomIdGenerated-12323D123" : "Hi, HOw are you",
}
So in your case your first approach is good enough! Since you dont need unique node for counting number of events added.

What is a CloudKit RecordID

While starting to work more with CloudKit I just realized that I don't actually know what a RecordID is...
I'm looking at the CloudKit dashboard right now, I see RecordTypes, RecordName etc, but I don't see RecordID. The iOS Dev library mentions RecordID a lot, but never actually tells what it is or where to find it.
I guess I'm just dumb, but I can't figure it out.
Every record has a record Id, which is a CKRecordID instance, and the class has a name property. If you don't specify a name, new records will have a record Id with a name that is a GUID.
You can only fetch with record Id if you know it, and in most instances you will let Cloud Kit create on for you, and you won't store it locally, so you won't know it.
Every CKRecord has a bunch of metadata, which includes the record id. See the list here.
Here a snapshot of the dashboard, your recognised it. You can either accept the recordIDs that CloudKit gives you or generate your own [although they must be unique within your database instance]. Here the screenshot
And here a small code snippet to show you how to create your own ID, using the same method I suspect CloudKit uses.
let uniqueReference = NSUUID().UUIDString
let uniqueRecordID = CKRecordID(recordName: uniqReference)
let newRecord = CKRecord(recordType: "Collection", recordID:uniqueRecordID)