getDocuments() function getting the same data more than one time - ios

I am migrating from relational DB to NOSQL, and I already have read the entire Firebase Documentation, and now I am hands on to a study project to learn more about it.
What I am trying to do is: I have an user_profile where each user created by Auth will get one document.
Inside this document I have an array called groups that shows all groups this user has joined.
I need to retrieve a list of this groups according to the user logged in.
Then I am using getDocuments() to retrieve this information from the DB.
What I need to get is a array of string with the group_id (as per second pic).
The my current code is retrieving the information I need, but it get all the ids as one object. So if I have 4 groups, it will retrieve 4 objects containing the all 4 groups the user has joined.
func getGroups() -> [String] {
let currentUser = Auth.auth().currentUser?.uid
let db = Firestore.firestore()
var groups = [""]
var groupsArray = [""]
db.collection(K.Collections.userProfile)
.whereField(K.DBFields.UserProfile.userId, isEqualTo: currentUser!)
.getDocuments { (snapshot, error) in
if let error = error {
print(error)
} else {
for document in snapshot!.documents {
groups = (document.get("groups")) as! [String]
//groupsArray.append(groups)
print("Group ID: \(groups)")
}
}
}
return groups
}
And the result I am getting is the one below:
roup ID: ["8m0W7cQLuSjQCJes2fpL", "l84GnZSpIUs43cXO13Qm", "unb0LPYOttDN6WogRXDt", "ohG09dwyVrAd6GcXa6mx"]
Group ID: ["8m0W7cQLuSjQCJes2fpL", "l84GnZSpIUs43cXO13Qm", "unb0LPYOttDN6WogRXDt", "ohG09dwyVrAd6GcXa6mx"]
Group ID: ["8m0W7cQLuSjQCJes2fpL", "l84GnZSpIUs43cXO13Qm", "unb0LPYOttDN6WogRXDt", "ohG09dwyVrAd6GcXa6mx"]
Group ID: ["8m0W7cQLuSjQCJes2fpL", "l84GnZSpIUs43cXO13Qm", "unb0LPYOttDN6WogRXDt", "ohG09dwyVrAd6GcXa6mx"]
I have tried so many different approaches to try to fetch the correct data, but hasn't work.
Once again, I just want to get this array as result:
var groups = [
"8m0W7cQLuSjQCJes2fpL",
"l84GnZSpIUs43cXO13Qm",
"unb0LPYOttDN6WogRXDt",
"ohG09dwyVrAd6GcXa6mx"]
Thanks a lot
Leonardo D'Amato

You're retrieving the values from groups as [String] (string array). But then you print that string array as a single value.
If you want to access the individual elements of the [String] in Swift, you can loop over it:
groups = (document.get("groups")) as! [String]
for group in groups {
print("Group ID \(group).")
}
Also see the Swift documentation on accessing array values.

I don't have much idea of Swift but in "node" there were different results when I tried to use for in place of map You should use the following:
let dataDescription = snapshot.data().map(String.init(describing:)) ?? "nil"
print("Document data: (dataDescription)")
Shown here

Related

Database structure in Firebase Swift

I am trying to think of the best solution for storing user's data in firebase, considering scalability, queries etc...
For example: Each user can create a list of categories. I was thinking of storing it like so:
Categories (Collection)
- User ID (Document)
- Category ID
- title
- imageURL
I would like to access the categories collection, find a list of all user ID's and when accessing a user ID I find their list of all the categories they have created...
I am quite new to firebase...
Or should I create Categories(collections) - User ID (document) - Category Id (collection) - title etc (document) ? Just does not seem right....
I appreciate any assistance or tips!
Edit: This is how I am currently saving the data to Firebase from my IOS app:
Model:
struct Category: Codable, Identifiable {
#DocumentID var docId: String?
var id: String? = UUID().uuidString
var authorId: String
var title: String
var categoryImageUrl: String?
Save data function:
guard let userId = Auth.auth().currentUser?.uid else { return } // REMOVE
let db = Firestore.firestore() // REMOVE
let collectionRef = db.collection("Categories").document(userId)
do {
try collectionRef.setData(from: category)
}
catch {
print(error)
}
It's hard to give a comprehensive answer, but one thing to keep in mind is that the client-side SDKs for Firestore don't have an API to get a list of (sub)collection. So it's important that your application can know the collection names without that, either because they are hard-coded (most common) or because they are stored elsewhere (such as in the parent document).

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!

Swift Query Into Firebase Firestore Nested Array

I have nested data that I'd like to display in a tableView.
My data is structured like so...
/users
/userid
name: "John"
age: 23
/likedPosts
0:post1
1:post2
For the tableview I'd like to display these posts (which have their own collection of data).
In order to do that I need to...
1) Get the count of the array and
2) Query the users likedPost array values to get the content of the post.
I'm currently using the getDocument function and can't figure it out.
for example...
func getUserLikedPosts() {
if let user = Auth.auth().currentUser {
let userFS = Firestore.firestore().collection("users").document(user.uid)
userFS.getDocument(completion: { (snapshot, error) in
print(snapshot?)
})
}
}
This doesn't even print out the nested array?
Something like this should work:
func getUserLikedPosts() {
if let user = Auth.auth().currentUser {
let userFS = Firestore.firestore().collection("users").document(user.uid).collection(“likedPosts”)
userFS.getDocuments(completion: { (snapshot, error) in
print(snapshot?)
})
}
}
With Firestore you need to drill down to the actual node you want, unlike Firebase where you can access child snapshots.
So after a quick discussion, with the OP, it was that the user in question didn’t have a likedPost object.
To access the array (Please be aware I haven’t tested this code, it is an example):
If let doc = document, let array = doc[“likedPost] as NSArray {
print(array)
}

Group chat - How to populate a table view with only the chat rooms a user is involved in?

I'm trying to make a group chat app, and so far I have the app setup (from a tutorial) so that any user can create a room, and all of those rooms show up in a central list, which is the same for all users. Now I need to alter this so that users only see rooms that they created, or have been added to (just like any messaging app like iMessage).
This is how a room is created and appended to the Room object (and uploaded to Firebase):
var rooms: [Room] = []
// Create new room
#IBAction func createRoom(_ sender: Any) {
if let name = newRoomTextField?.text {
let newRoomRef = roomRef.childByAutoId()
let roomItem = [
"name": name
]
newRoomRef.setValue(roomItem)
}
}
private func observeRooms() {
// Observe method to listen for new channels being written to Firebase
roomRefHandle = roomRef.observe(.childAdded, with: { (snapshot) -> Void in
let roomData = snapshot.value as! Dictionary<String, AnyObject>
let id = snapshot.key
if let name = roomData["name"] as! String!, name.characters.count > 0 {
self.rooms.append(Room(id: id, name: name))
self.tableView.reloadData()
} else {
print("Error! Could not decode channel data")
}
})
}
Room object:
internal class Room {
internal let id: String
internal let name: String
init(id: String, name: String) {
self.id = id
self.name = name
}
}
Currently the table view of rooms is populated with cell.textLabel?.text = rooms[(indexPath as NSIndexPath).row].name (cellForRow method), and return rooms.count (numberOfRows method), thus returning all of the rooms in the app, not specific to the user.
This is where I'm not sure how to proceed: I'm guessing I'll add an array "participants: [String]" to my Room object (in addition to "id" and "name"), then in observeRooms when I self.rooms.append(Room(id: id, name: name)), I'll also append the user's id to "participants"? Then populate the table view with only the rooms that the current user's id is a participant in?
Is this the right approach?
Thanks for any help/guidance!
I think this is the right approach, you basically have 2 choices :
Store a participants array into each room, which allow you to query rooms where this array contains our current userId.
Store a rooms array on each user, allowing to query rooms directly from this data.
The first approach seems to make more sense in case you'd like to also display a list of participants, though the query may take longer to execute than the second approach for many rooms * many participants.
You could also use both approach at the same time to have both your functionality and a faster query time for the user rooms.

containedIn query for Firebase

Coming from Parse, I have heavily relied on the containedIn query to collect the right data. In Parse, I might have had an array of objectIds and queried for all objects with those ids. I am looking to achieve the same on Firebase.
I understand that it is important to flatten data, but I don't see how this helps with the problem. Let's say I have a chat room with a list of users inside it. I collect that data and now have an array of usernames. I would like to now navigate to the users in the database and retrieve all that match one element inside this username array. How can I accomplish something like this?
For example, a set of users in a official Firebase example:
{
"users": {
"alovelace": { ... },
"ghopper": { ... },
"eclarke": { ... }
}
}
I would like to perform a query to download the following users:
["alovelace", "eclarke"]
While a general answer would be helpful, an answer in Swift would be best. Thank you.
An example is that they are the two members of a chat room. Or that
the current user is following them.
So a theoretical users node
users
alovelace
followed_by
bill: true
frank: true
in_chat_room: room_42
location: France
ghopper
followed_by
jay: true
in_chat_room: room_27
location: USA
eclarke
followed_by
frank: true
in_chat_room: room_42
location: Canada
and some chat rooms
chat_rooms
room_27
ghopper: true
room_42
lovelace: true
eclarke: true
To get the detailed users nodes of users in chat room 42 (lovelace, eclarke)
let usersRef = self.myRootRef.childByAppendingPath("users")
usersRef.queryOrderedByChild("in_chat_room").queryEqualToValue("room_42")
.observeEventType(.Value, withBlock: { snapshot in
for child in snapshot.children {
let location = child.value["location"] as! String
print(location) //prints France and Canada
}
})
To get the users Frank is following (lovelace, eclarke):
let usersRef = self.myRootRef.childByAppendingPath("users")
usersRef.queryOrderedByChild("followed_by/frank").queryEqualToValue(true)
.observeEventType(.Value, withBlock: { snapshot in
for child in snapshot.children {
let userName = child.key as String
print(userName)
}
})
Note that using user names as node key's is generally a bad idea - they should be stored by their uid.
Also note we didn't really do anything with the chat_rooms node but to maintain the relationship, having nodes refer to each other can be useful for observing changes etc.
Edit:
In response to a comment, here's the structure for each user to show who they are following instead of who is following the user
users
alovelace
following_user
bill: true
frank: true
in_chat_room: room_42
location: France
with this structure, alovelace is following bill and frank.
To get all of the users following frank:
let usersRef = self.myRootRef.childByAppendingPath("users")
usersRef.queryOrderedByChild("following_user/frank").queryEqualToValue(true)
.observeEventType(.Value, withBlock: { snapshot in
for child in snapshot.children {
let userName = child.key as String
print(userName)
}
})

Resources