iOS : Firebase Send/Save Data in chunks - ios

I have trying to save data in firebase like this
class FirebaseManager {
static let shared = FirebaseManager()
private let tableRef = Database.database().reference(withPath: "XYZDemo")
func add(item: HealthData) {
self.tableRef.childByAutoId().setValue(item.dictionary)
}
}
This code save data one bye one.
How can i add more than one data at a time i.e save 5 values at once?

To simultaneously write to specific children of a node without overwriting other child nodes, use the updateChildValues method.
Basicly you create an array with the updates/writes you want to perform and write that array to Firebase. Here is an example showing 1 post being written to two different locations:
//Create the new key
let key = self.tableRef.childByAutoId().key
//Post data that is going to be written
let post = ["uid": userID,
"author": username,
"title": title,
"body": body]
//Create the array with (two) updates
let childUpdates = ["/posts/\(key)": post,
"/user-posts/\(userID)/\(key)/": post]
//Write the array to the database
ref.updateChildValues(childUpdates)
A couple things to keep in mind when you are dong this:
It's all or nothing, either all writes fail or all writes succeed. If even 1 of the writes in the array fails then all writes will fail.
Make sure you use the correct reference for each write because it will override all the data at that location.

Related

Update multiple Firebase documents using an array of Document IDs in Swift

I have an array of Strings which represent Firebase Document IDs like so:
var idArray = [“PuLDb90jgz3a5P8bLQoy”, “PMKoZIp46umXQnUlA64a”, “cVGbD3Wy4gWjZ9fZP7h1”]
This Array is dynamic and has been generated by a previous getDocuments call. It could have up to 15 ID strings in it, so it cannot be hard coded.
Within each Firebase Document I have an Int field called menuPosition set to a current value.
What I am trying to do is update each document which appears in the idArray and simply -1 from the menuPosition field value in one go.
What should be so straightforward is driving me crazy - does anyone know a simple way to do it? Obviously, if I put it in a for loop then the code will run too many times and my menuPosition value would be wrong. I just want each operation to perform once.
Any help would be greatly appreciated
Just run a loop over the document IDs and decrement the field in each iteration.
var idArray = ["J2LovReBF0v8F4e0RSBU", "UcW8tsgld2ZuUo92xfP8", "oHTJ4iO1NWCK7x1aryne"]
for docId in idArray {
Firestore.firestore().document("someCollection/\(docId)").updateData([
"menuPosition": FieldValue.increment(Int64(-1))
])
}
If you want this operation to be atomic then just wrap it in a batch write.
var idArray = ["J2LovReBF0v8F4e0RSBU", "UcW8tsgld2ZuUo92xfP8", "oHTJ4iO1NWCK7x1aryne"]
let db = Firestore.firestore()
let batch = db.batch()
for docId in idArray {
batch.updateData([
"menuPosition": FieldValue.increment(Int64(-1))
], forDocument: db.document("someCollection/\(docId)"))
}
batch.commit()

How to structure a search list in Firestore?

I want to show a list of artists in my app which the user will be able to search through. I'm not sure however how to save this in Firestore?
First I created a collection "searchLists" with a document for each DJ but that means a lot of document reads so that's out of the question.
Now I created a document called "artists" which has a field "artistsDictionary" which contains all the artists.
| searchLists (collection)
* artists (document)
- artistsArray (array)
0: (map)
name: "Artist 0" (string)
1: (map)
name: "Artist 1" (string)
2: (map)
name: "Artist 2" (string)
And I retrieve and parse the array as followed:
let docRef = db.collection("searchLists").document("artists")
docRef.getDocument { (document, error) in
if let document = document, document.exists {
guard let documentData = document.data() else { return }
let artistsDictionaryArray = documentData["artistsArray"] as? [[String: Any]] ?? []
let parsedArtists = artistsDictionaryArray.compactMap {
return SimpleArtist(dictionary: $0)
}
self.artistsArray = parsedArtists
} else {
print("Document does not exist")
}
}
(SimpleArtist is a struct containing a "name" field.)
And I mean, it works, but I'm still new to Firestore and this seems kinda off. Is it? Or is this how I should/could do it?
First I created a collection "searchLists" with a document for each DJ but that means a lot of document reads so that's out of the question.
This is the right approach, so you should go ahead with it.
Why do I say that?
According to the official documentation regarding modeling data in a Cloud Firestore database:
Cloud Firestore is optimized for storing large collections of small documents.
Storing data in an array is not a bad option but this is most likely used, let's say to store favorite djs. I say that because the documents have limits in Firestore. So there are some limits when it comes to how much data you can put into a document. According to the official documentation regarding usage and limits:
Maximum size for a document: 1 MiB (1,048,576 bytes)
As you can see, you are limited to 1 MiB total of data in a single document. When we are talking about storing text, you can store pretty much but as your array getts bigger, be careful about this limitation.
First off, Alexs' answer is 100% correct.
I want to add some additional data points that may help you in the long run.
The first item is arrays. Arrays are very challenging in NoSQL databases - while they provide a logical sequence data via the index, 0, 1, 2 they don't behave like an array in code - so for example; Suppose you wanted to insert an item at an index. Well - you can't (*you can but it's not just a simple 'insert' call). Also, you can't target array elements in queries which limits their usefulness. The smallest unit of change in a Firestore array field is the entire field - smaller changes to individual elements of a field can't be made. The fix is to not use arrays and to let FireStore create the documentID's for you data 'objects' on the fly e.g. the 'keys' to the node
The second issue - (which may not be an issue currently) is how the data is being handled. Suppose you release your app and a user has 2 million artists in their collection - with your code as is, all of that data is downloaded at one time which will probably not be the best UI experience but additionally, it could overwhelm the memory of the device. So working in 'chunks' of data it a lot easier on the device, and the user.
So I put together some sample code to help with that.
First a class to store your Artist data in. Just keeps track of the documentID and the artist name.
class ArtistClass {
var docId = ""
var name = ""
init(aDocId: String, aName: String) {
self.docId = aDocId
self.name = aName
}
}
and a class array to keep the artists in. This would be a potential dataSource for a tableView
var artistArray = [ArtistClass]()
This is to write an artist as a document instead of in an array. The documentID is a FireStore generated 'key' that's created for each artist.
func writeArtists() {
let artistsRef = self.db.collection("artists")
let floyd = [
"name": "Pink Floyd"
]
let zep = [
"name": "Led Zeppelin"
]
let who = [
"name": "The Who"
]
artistsRef.addDocument(data: floyd)
artistsRef.addDocument(data: zep)
artistsRef.addDocument(data: who)
}
and then function to read in all artists.
func readArtists() {
let artistsRef = self.db.collection("artists")
artistsRef.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let docId = document.documentID
let name = document.get("name") as! String
let artist = ArtistClass(aDocId: docId, aName: name)
self.artistArray.append(artist)
}
for a in self.artistArray { //prints the artists to console
print(a.docId, a.name)
}
}
}
}
So your data in Firestore looks like this
artists (collection)
8lok0a0ksodPSSKS
name: "Let Zeppelin"
WKkookokopkdokas
name: "The Who"
uh99jkjekkkokoks
name: "Pink Floyd"
so then the cool part. Suppose you have a tableView that shows 10 artists at a time with a down button to see the next 10. Make this change
let artistsRef = self.db.collection("artists").order(by: "name").limit(to: 10)
Oh - and you'll notice the function of sorting now goes the server instead of the device - so if there's a million artists, it's sorted on the server before being delivered to the device which will be significantly faster.
You can also then more easily perform queries for specific artist data and you won't need to be as concerned about storage as each artist is their own document instead of all artists in one.
Hope that helps!

How do I create a new collection?

I'm building a chat app where there are GroupConversations and GroupMessages. Following the example of some of the Firestore YouTube videos, I've chosen to structure my data this way:
The ID of the GroupConversation is the ID for the collection of GroupMessages associated with that GroupConversation. Then inside of that document should be a new collection called Messages which holds documents.
Does that make sense or am I overcomplicating things? I can't seem to create that Messages collection or set anything to it in my Swift code
GroupConversations
id
title
desc
memberIds
GroupMessages
GroupConversationID
createdAt: Date // just because I need a key and a document?
Messages (new collection)
Message1
Message2
Message3
Thanks
Edit: Added Swift code
let db = Firestore.firestore()
var data: [String: Any] = [
"text": title,
"senderUsername": username,
"createdAt": Date(),
"updatedAt": Date()
]
let creationData: [String: Any] = [
"text": "Group Created",
"createdAt": Date()
]
let doc = db.collection("GroupMessages").addDocument(data: creationData)
db.document("GroupMessages/\(groupConvo.documentId)/\(doc.documentID)/messages").setData(data) { error in
if error == nil {
completion()
}
}
In my console, I'm seeing a new GroupMessage object created but not sub-collection of that GroupMessage called messages
You're not building the path to the document correctly. Document paths always alternate between collection and document names. To build a path to a document in a subcollection, it would go collection/document/subcollection/document. It looks like you've mixed up the last two elements in the string you're building.
In my opinion, it's harder to go wrong if you build up the document using methods rather than trying to build a string. Something like this:
db
.collection("GroupMessages")
.document(groupConvo.documentId)
.collection("Messages")
.document(doc.documentId)
.setData(...)

Firebase - atomic write of multiple values to multiple locations

I need to register an object to Firebase. The object has multiple fields, and I need to write them all to multiple paths.
My code :
public func RegisterProductOnDatabase(database dataBase: DatabaseReference)
{
// Run in one transaction
RegisterProductOnDatabase(database: dataBase)
RegisterProductForAllUsers(database: dataBase)
}
private func RegisterProductForAllUsers(database dataBase: DatabaseReference)
{
dataBase.child("Products").child(self.UniqueID()).child("Name").setValue(self.Name())
dataBase.child("Products").child(self.UniqueID()).child("UniqueID").setValue(self.UniqueID())
dataBase.child("Products").child(self.UniqueID()).child("Price").setValue(self.Price())
dataBase.child("Products").child(self.UniqueID()).child("Description").setValue(self.Description())
dataBase.child("Products").child(self.UniqueID()).child("ToBuy?").setValue(self.m_ToBuy)
dataBase.child("Products").child(self.UniqueID()).child("ToSell?").setValue(self.m_ToSell)
dataBase.child("Products").child(self.UniqueID()).child("Owner").setValue(self.m_Owner)
dataBase.child("Products").child(self.UniqueID()).child("Amount").setValue(self.m_Amount)
dataBase.child("Products").child(self.UniqueID()).child("MainImage").setValue(self.m_PicturesURLs.first)
}
private func RegisterProductForAddingUser(database dataBase: DatabaseReference)
{
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("Name").setValue(self.Name())
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("UniqueID").setValue(self.UniqueID())
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("Price").setValue(self.Price())
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("Description").setValue(self.Description())
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("ToBuy?").setValue(self.m_ToBuy)
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("ToSell?").setValue(self.m_ToSell)
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("Amount").setValue(self.m_Amount)
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("MainImage").setValue(self.m_PicturesURLs.first)
}
I need the function "RegisterProductOnDatabase" to run as one transaction - meaning all written values to be written as one transaction.
1) How can I write all this data as ONE transaction using Swift ?
2) Is there a better way to write all these values without code multiplication?
Firstly, you should use dictionaries instead of manually modifying each node.
This is from the Firebase documentation:
let key = ref.child("posts").childByAutoId().key
let post = ["uid": userID,
"author": username,
"title": title,
"body": body]
let childUpdates = ["/posts/\(key)": post,
"/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)
Read more about writing data here.
Secondly, in order to modify in multiple locations, use this answer as I have recently had the same question.

Firebase load related data iOS

What's the best way to load "related" data in swift?
Common setup, if I have a list of users all stored under uid node and contains a list of follows which stores uids, something like:
"users" : {
"abc123" : {
"email" : "test#test.com",
"follows" : {
"xyz789" : true
}
},
"xyz789" : { ... }
}
What's the most efficient way of loading in the data for all the users one user follows? Is it best to loop through each of the uid's with observeSingleEvent(of: .value)?
This is the solution I've come up with, but feels somewhat cumbersome:
func loadRelated(user: User, completion: #escaping (Bool, [UserObject]) -> ()) {
let ref = Database.database().reference(withPath: "users/" + user.uid + "/follows")
ref.observeSingleEvent(of: .value) { snapshot in
var uids = [String]()
for child in snapshot.children {
let userData = child as! DataSnapshot
uids.append(userData.key)
}
let userRef = Database.database().reference(withPath: "users")
var users = [UserObject]()
var count = 0
uids.forEach { uid in
userRef.child(uid).observeSingleEvent(of: .value) { snapshot in
let user: UserObject(from: snapshot)
users.append(user)
count += 1
if count == uids.count {
completion(true, users)
}
}
}
}
}
I don't really want to go down the denormalization path and store each users data under the top level user.
If you are decided on using Realtime Database, it is best practice to create another root node in your case called user-follows. You can create a follow at the path user-follows/$uid/$fid by setting the value to true, then on your app you would have to observeSingleEvent for each snapshot key ($fid) at user-follows/$uid.
To avoid having to observe each follow separately, instead of setting the value to true, you can just store the data you need about a user in user-follows/$uid. However, a user may change their username for example and so you would need to keep the data inside each user-follows up to date. You can utilise Firebase Cloud Functions to maintain the user-follows when a user changes their information.
Otherwise, I would suggest looking at Firebase Firestore, where some nesting is allowed.
If you know that your node at /users will always contain few users, you could try to get all the users at once with a observeSingleEvent(of:) at path /users. Then filter the users with the ones who are in ../follows.
This may pull more data but it might be faster (not sure) and will need less code to handle.
In fact your initial implementation is quite performant already. Just make sure to handle correctly failing of observeSingleEvent(of:) or the condition count == uids.count will never be fulfilled.
By the way storing each user under ../follows will just duplicate your data and will be hard to maintain updated. So yes avoid it.

Resources