Firestore Swift: Get number of nested maps stored in a document - swift

I have a document with fields structured something like this:
Is there any way to count the number of nested maps? So in this case, I want to show that I have two "elements",
JGBQBnFX23Mh5NL4W8f3N1E5Czu1 and JGBQBnFX23Mh5NL4W8f3N1E5Czu1
Right now, I only know how to retrieve everything under "attendees" using
db.collection("Collection")
.document(eventID)
.getDocument { doc, err in
if let err = err {
print(err.localizedDescription)
return
}
let data = doc?.data()
let attendees = data?["attendees"]
print(attendees)
}
But I do not know how to get the total count (of my document IDs)
Keep in mind I will not be able to use arrays in my scenario. Thanks!

It turns out it could be fixed by changing
let attendees = data?["attendees"]
to
let attendees = data?["attendees"] as? Dictionary<String, Any>
and then you could get the total count by doing attendees?.count

Related

(Swift) How to retrieve array -> mapped object data from Firestore Cloud [duplicate]

In Swift, to retrieve an array from Firestore I use:
currentDocument.getDocument { (document, error) in
if let document = document, document.exists {
let people = document.data()!["people"]
print(people!)
} else {
print("Document does not exist")
}
}
And I receive data that looks like this
(
{
name = "Bob";
age = 24;
}
)
However, if I were to retrieve the name alone, normally I'd do print(document.data()!["people"][0]["name"]).
But the response I get is Value of type 'Any' has no subscripts
How do I access the name key inside that object inside the people array?
The value returned by document.data()!["people"] is of type Any and you can't access [0] on Any.
You'll first need to cast the result to an array, and then get the first item. While I'm not a Swift expert, it should be something like this:
let people = document.data()!["people"]! as [Any]
print(people[0])
A better way of writing #Frank van Puffelen's answer would be:
currentDocument.getDocument { document, error in
guard error == nil, let document = document, document.exists, let people = document.get("people") as? [Any] else { return }
print(people)
}
}
The second line may be a little long, but it guards against every error possible.

Get sorted Firestore documents when denormalizing data

I am using Firestore for my app on which users can publish Posts, stored in the posts collection:
posts
{postID}
content = ...
attachementUrl = ...
authorID = {userID}
Each User will also have a timeline in which the posts of the people they follow will appear. For that I am also keeping a user_timelines collection that gets populated via a Cloud Function:
user_timelines
{userID}
posts
{documentID}
postID = {postID}
addedDate = ...
Because the data is denormalized, if I want to iterate through a user's timeline I need to perform an additional (inner) query to get the complete Post object via its {postID}, like so:
db.collection("user_timelines").document(userID).collection("posts")
.orderBy("addedDate", "desc").limit(100).getDocuments() { (querySnap, _) in
for queryDoc in querySnap.documents {
let postID = queryDoc.data()["postID"] as! String
db.collection("posts").document("postID").getDocument() { (snap, _) in
if let postDoc = snap {
let post = Post(document: postDoc)
posts.append(post)
}
}
}
}
The problem is by doing so I am loosing the order of my collection because we are not guaranteed that all the inner queries will complete in the same order. I need to keep the order of my collection as this will match the order of the timeline.
If I had all the complete Post objects in the timeline collections there would be not issue and the .orderBy("addedDate", "desc").limit(100) would work just fine keeping the Posts sorted, but If I denormalize I cant seem to find a correct solution.
How can I iterate through a user's timeline and make sure to get all the Post objects sorted by addedDate even when denormalizing data?
I was thinking of creating a mapping dictionary postID/addedDate when reading the postIDs, and then sort the Post at the end using this dictionary, but I am thinking there must be a better solution for that?
I was expecting this to be a common issue when denormalizing data, but unfortunately I couldnot find any results. Maybe there's something I am missing here.
Thank you for your help!
What you can do is enumerate the loop where you perform the inner query, which simply numbers each iteration. From there, you could expand the Post model to include this value n and then sort the array by n when you're done.
db.collection("user_timelines").document(userID).collection("posts").orderBy("addedDate", "desc").limit(100).getDocuments() { (querySnap, _) in
for (n, queryDoc) in querySnap.documents.enumerated() {
let postID = queryDoc.data()["postID"] as! String
db.collection("posts").document("postID").getDocument() { (snap, _) in
if let postDoc = snap {
let post = Post(document: postDoc, n: n)
posts.append(post)
}
}
}
posts.sort(by: { $0.n < $1.n })
}
The example above actually won't work because the loop is asynchronous which means the array will sort before all of the downloads have completed. For that, consider using a Dispatch Group to coordinate this task.
db.collection("user_timelines").document(userID).collection("posts").orderBy("addedDate", "desc").limit(100).getDocuments() { (querySnap, _) in
let dispatch = DispatchGroup()
for (n, queryDoc) in querySnap.documents.enumerated() {
dispatch.enter() // enter on each iteration
let postID = queryDoc.data()["postID"] as! String
db.collection("posts").document("postID").getDocument() { (snap, _) in
if let postDoc = snap {
let post = Post(document: postDoc, n: n)
posts.append(post)
}
dispatch.leave() // leave no matter success or failure
}
}
dispatch.notify(queue: .main) { // completion
posts.sort(by: { $0.n < $1.n })
}
}

Swift: Wait for firestore load before next load

I have a view controller that lists data from a firestore database. Inside a firestore collection, I have a bunch of documents with the information shown in the list, and one document called order which contains one field which is an array of strings in the order I want them displayed. My code grabs this:
self.db.collection("officers").document(school).collection(grade).document("order").getDocument {(document, error) in
if let document = document, document.exists {
self.officerNames = (document.data()!["order"] as! Array<String>)
and then is supposed to use the strings in the array order (officerNames) to query the documents in that same collection (all the documents have a different role so it's only getting one document in the snapshot) and display them in the same order as the one set in order (officerNames).
for item in 1...self.officerNames.count {
self.db.collection("officers").document(school).collection(grade).whereField("role", isEqualTo: self.officerNames[item-1]).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let officerMessage = document.data()["agenda"] as! String
let officerInfo = document.data()["short"] as! String
(a bunch of code here using that ^ ^ and due to the color I need item to be an integer)
}
}
}
}
I know that if I try printing item before the self.collection("officers")..... the numbers count by one but if I do that in the for document in querySnapshot..... they're all out of order meaning some documents are loaded faster than others. I have read about Async functions in Swift (although I do use those in JavaScript) but am really confused how to use them and hopefully, there is a simpler way to do this. Any way I can wait to make sure the previous document has been loaded and analyzed before iterating through the loop again?
Here's a screenshot of the database:
Honestly, you may want to examine your data structure and see if you can create one that doesn't require multiple queries like this. I can't quite tell what your data structure is, but if you update your question to include it, I can give some suggestions for how to refactor so you don't have to do 2 different get requests.
That being said, since Swift doesn't have promises like JS, it can be tough to keep data in order. For most cases, closures work well, as I wrote about in this blog. But they still won't preserve order in an array of async calls. Assuming you're using some array to store the officer's data, you can declare the size of the array up front by giving each one a default value. This would look something like this:
var officerArray = [Officer](repeating:Officer(), count: self.officerNames.count)
Of course, it'll be different depending on what kind of objects you're populating it with. I'm using some generic Officer object in this case.
Then, rather than appending the newly created Officer object (or whatever you're calling it) to the end of the array, add its value to its particular location in the array.
for item in 1...self.officerNames.count {
self.db.collection("officers").document(school).collection(grade).whereField("role", isEqualTo: self.officerNames[item-1]).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let officerMessage = document.data()["agenda"] as! String
let officerInfo = document.data()["short"] as! String
// etc
officerArray[self.officerNames.count-1] = Officer(officerMessage: officerMessage, officerInfo: officerInfo) // or however you're instantiating your objects
}
}
}
}
This preserves the order.

Firebase Firestore Messing Up Order Of Array

I'm using Firebase's Firestore to store data for my iOS app. In Firestore, I have two collections. One called songs and one called playlists. The songs collection contain many documents and each document has a single song info inside of it. Here's what a songs document looks like.
Then, in my collection playlists it contains some documents which are playlists. Here's an example of a playlists document.
I want to be able to display a playlist, and then get the songTitles from that playlist from my songs collection. So, I've done this:
var testPlaylistTitles = [String]()
db.collection("playlists").whereField("title", isEqualTo: "Test Playlist").getDocuments { (snapshot, error) in
for document in (snapshot?.documents)! {
self.testPlaylistTitles = ((document.data()["songTitles"] as? [String])!)
}
}
This makes the variable testPlaylistTitles = ["Test Song", "Test Song 2", "Test Song 3"]. And that's great. But this is where it goes wrong. I want to get information about each one of these songs from my songs collection. So, I create a for loop and loop through the songs to append to my other arrays of artist, images, and titles. Just for this example, I'll use a variable of returedTitles.
var testPlaylistTitles = [String]()
db.collection("playlists").whereField("title", isEqualTo: "Test Playlist").getDocuments { (snapshot, error) in
for document in (snapshot?.documents)! {
testPlaylistTitles = ((document.data()["songTitles"] as? [String])!)
}
var returedTitles = [String]()
for i in 0...testPlaylistTitles.count-1 {
db.collection("songs").whereField("title", isEqualTo: testPlaylistTitles[i]).getDocuments(completion: { (snapshot, error) in
for document in (snapshot?.documents)! {
returedTitles.append((document.data()["title"] as? String)!)
}
})
}
}
Now, returedTitles is equal to a shuffled version of the original testPlaylistTitles. I thought at first that somehow it was ordering testPlaylistTitles by alphabetical order, but it isn't. Does anyone have any ideas on what is happening and how to fix it! Thanks a lot! This has been stumping me for a while.
I have no idea of how to do this in swift, but I would change the data structure of the songs on the playlist document to a object where the songs are the keys and the order the value, then, in the second loop in your query, you use the keys to return the values and you should be able to sort by the values.
{
description: "test",
songs: {
"Test Song 1": 1,
"Test Song 2": 3,
"Test Song 4": 2
},
title: "Test playlist"
}
What I may also suggest is to use the ID's of the songs in the list, as you are retrieving the data anyway at a later stage.
It looks like you're depending on the order of execution of the queries in the for loop. Bear in mind that getDocuments() is asynchronous, meaning it returns immediately, and the results appears in the callback some time later (there is no guarantee how long it will take). Because it's asynchronous, you're effectively kicking off testPlaylistTitles.count nubmer of queries all happening at the same time, each without regard to the other. As a result, the array you're building is going to accumulate the results in an undefined order. To help visualize this, put a log message in each callback to see what order they're actually being invoked. Include contents of the array each time as well, to see how the array is being built.
If the order of the results matters, you will need to figure out what that order is yourself. If that means performing the queries one after the other in order (which would be an overall performance hit), then you'll have to code it that way.
But the bottom line is that you'll need to think carefully about doing asynchronous work. Please read this blog for more information about why Firebase APIs are asynchronous.
As Doug points out, the issue is that the calls are asynchronous, so they won't necessarily be added to the array in the order you start them in--they are added when the data becomes available. There are a few ways to resolve this, but one suggestion to get you started would be to initialize your arrays such that their size is equal to the size of testPlaylistTitles. Then, when you get the data, you insert it in that specific location in the array instead of appending it to the end of the array.
var testPlaylistTitles = [String]()
db.collection("playlists").whereField("title", isEqualTo: "Test Playlist").getDocuments { (snapshot, error) in
for document in (snapshot?.documents)! {
testPlaylistTitles = ((document.data()["songTitles"] as? [String])!)
}
var returedTitles = [String](repeating: "title", count: testPlaylistTitles.count)
for i in 0...testPlaylistTitles.count-1 {
db.collection("songs").whereField("title", isEqualTo: testPlaylistTitles[i]).getDocuments(completion: { (snapshot, error) in
if (snapshot!.documents.count) > 0 {
let doc = (snapshot?.documents.first)!
returedTitles[i] = ((doc.data()["title"] as? String)!)
}
})
}
}
In this code, I've initialized an array the size of testPlaylistTitles. The value of each index is just "title" to begin with, but is then replaced with the value from Firestore once it is downloaded.

How do I retrieve a random object from Firebase using a sequential ID?

I'm looking for an easy way to query my database in firebase using swift to retrieve a random object. I've read a lot of threads and there doesn't seem to be an easy way. One example showed it can be done be creating a sequential number but there's no information on how to create this sequential number for each record.
So either I need information on how to create a sequential number each time a record is created or if someone knows an easy way to retrieve a random record from a database that would be very helpful. In swift preferably.
My Database structure:
QUERY RANDOM OBJECT IN FIREBASE < VERY SIMPLE SOLUTION > SWIFT 4
One thing that you could try is to restructure your data like this:
- profiles
- 1jon2jbn1ojb3pn231 //Auto-generated id from firebase.
- jack#hotmail.com
- oi12y3o12h3oi12uy3 //Auto-generated id from firebase.
- susan#hotmail.com
- ...
Firebase's auto-generated id's are sorted in lexicographical order by key, when they are sent to Firebase, so you can easily create a function like this:
func createRandomIndexForFirebase() -> String {
let randomIndexArray = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9"]`
let randomIndex = Int.random(in: 0..<randomIndexArray.endIndex)
//In lexicographical order 'A' != 'a' so we use some clever logic to randomize the case of any letter that is chosen.
//If a numeric character is chosen, .capitalized will fail silently.
return (randomIndex % 2 == 0) ? randomIndexArray[randomIndex] : randomIndexArray[randomIndex].capitalized
}
Once you get a random index you can create a firebase query to grab a random profile.
var ref: DatabaseReference? = Database.database().reference(fromURL: "<DatabaseURL>")
ref?.child("profiles").queryOrderedByKey().queryStarting(atValue: createRandomIndexForFirebase()).queryLimited(toFirst: 1).observeSingleEvent(of: .value, with: { snapshot in
//Use a for-loop in case you want to set .queryLimited(toFirst: ) to some higher value.
for snap in snapshot.children {
guard let randomProfile = snap as? DataSnapshot else { return }
//Do something with your random profiles :)
}
}
Database.database().reference().child("profiles").observeSingleEvent(of: .value) { (snapshot) in
if let snapshots = snapshot.children.allObjects as? [DataSnapshot] {
// do random number calculation
let count = snapshots.count
return snapshots[Int(arc4random_uniform(UInt32(count - 1)))]
}