Reading Firestore Document containing an array of references - swift

Thanks in advance for the help. I'm teaching myself Swift and trying to figure out how to retrieve the following data from Firebase. Here's my Firebase Data Model...
Groups (Collection)
-> GroupName (String)
-> Owner (References to someone in the Players collection)
Players (Collection)
-> PlayerFirstName
-> PlayerLastName
The Swift I've written to retrieve this data is in a ViewModel. getAllGroups is called from onAppear in the View and looks like this...
class Group: Identifiable, ObservableObject {
var id: String = UUID().uuidString
var name: String?
var owner: Player?
}
class GroupViewModel: ObservableObject {
#Published var groups = [Group]()
private var db = Firestore.firestore()
func getAllGroups() {
db.collection("groups").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No groups")
return
}
self.groups = documents.map { (queryDocumentSnapshot) -> Group in
var group = Group()
let data = queryDocumentSnapshot.data()
group.name = data["name"] as? String ?? ""
//
// LIKE --- SHOULD THIS CALL TO GETPLAYER use AWAIT, FOR EXAMPLE?
// WE'RE EXECUTING THE CLOSURE FOR THE FIRST CALL AND ABOUT TO MAKE A SECOND
//
group.owner = self.getPlayer(playerRef: data["owner"] as! DocumentReference)
return group
}
}
}
func getPlayer(playerRef: DocumentReference) -> Player {
var player = Player()
playerRef.getDocument { (document, error) in
guard error == nil else {
print ("error", error ?? "")
return
}
if let document = document, document.exists {
let data = document.data()
if let data = data {
player.firstName = data["firstname"] as? String
player.lastName = data["lastname"] as? String
}
}
}
return player
}
}
The sorta obvious problem here is the closure for retrieving the parent Group executes and then goes and tries to retrieve the Owner. But by the time the closure inside getPlayer completes... the Group has already been established.
Groups will have...
group[0]
-> GroupName = "Cool Name Here"
-> Owner = nil
group[0]
-> GroupName = "Different Cool Name"
-> Owner = nil
even though each Group definitely has an Owner.
I get there's some stuff here about asynchronous calls in Swift and how best to handle that... I'm just not sure what the proper pattern is. Thanks again for the help and advice!
-j

To restate the question:
How do you nest Firestore functions
There are 100 ways to do it and, a lot of it depends on the use case. Some people like DispatchGroups, others like escaping completion handlers but in a nutshell, they pretty much do the "same thing" as the following code, written out "long hand" for readability
func populateGroupArray() {
db.collection("groups").addSnapshotListener { (querySnapshot, error) in
guard let docs = querySnapshot?.documents else { return }
for doc in docs {
let groupName = doc.get("name") as! String
let ownerId = doc.get("owner_id") as! String
self.addToArray(groupName: groupName, andOwnerId: ownerId)
}
}
}
func addToArray(groupName: String, andOwnerId: String) {
db.collection("owners").document(andOwnerId).getDocument(completion: { snapshot, error in
let name = snapshot?.get("owner_name") as! String
let group = Group(groupName: groupName, ownerName: name)
self.groups.append(group)
})
}
In summary; calling populateGroupArray reads in all of the documents from the groups collection from Firestore (adding a listener too). We then iterate over the returned documents to get each group name and the owner id of the group.
Within that iteration, the group name and ownerId are passed to another function that reads in that specific owner via it's ownerId and retrieves the name
Finally, a Group object is instantiated with groupName and owner name being populated. That group is then added to a class var groups array.
Now, if you ask a Firebaser about this method, they will generally recommend not reading large amounts of Firebase data 'in a tight loop'. That being said, this will work very well for many use cases.
In the case you've got a HUGE dataset, you may want to consider denormalizing your data by including the owner name in the group. But again, that would be a rare situation.

Related

Swift + Firebase: Storing, retrieving, and using an array of document references

I'm relatively new to Swift and Firebase, so I'm not very familiar with the intricacies of how both work together. I'm building a chat app that has messages and threads. A user can send a message, struct Message, and if another user wants to directly reply to that message, a thread is created. For each message, I'm storing an array of Firebase document references to the other messages in the thread, threadBefore: [Message].
struct Message: Codable, Identifiable {
var id: String
var content: String
var name: String
var upvotes: Int
var likedByUser: Bool
var dontShow: Bool
var sentAt: Date
var threadsArray: [Message]
}
The following is my code for fetching all the chat messages from Firebase:
dontShow property: if dontShow == true means that the message is inside the thread and shouldn't be displayed like a regular message in the chat. However, the very last message in the thread is displayed and has dontShow = false.
func fetchMessages(docId: String, collectionType: String, isThreadMember: Bool) {
if (user != nil) {
db.collection("chatrooms").document(docId).collection(collectionType).order(by: "sentAt", descending: false).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No messages")
return
}
// let threadsTemp: [Message]()
let classroomId = docId
if !isThreadMember {
if collectionType == "messages" {
self.messages = documents.map { doc -> Message in
let data = doc.data()
let docId1 = doc.documentID
let content = data["content"] as? String ?? ""
let displayName = data["displayName"] as? String ?? ""
let likedUsersArr = data["likedUsers"] as? Array ?? [""]
// if message is in thread (but not last message), then don't show as normal message, but in thread
let dontShow = data["dontShow"] as? Bool ?? false
let sentAt = data["sentAt"] as? Date ?? Date()
let threadBefore = data["threadBefore"] as? [DocumentReference] ?? [DocumentReference]()
// using reference array
if dontShow == false {
if (threadBefore.count > 0) {
// reset the temporary array that holds the threads to be added afterwards
self.threadsTemp = []
for docRef in threadBefore {
docRef.getDocument { (doc2, error) in
if let doc2 = doc2, doc2.exists {
let docId2 = doc2.documentID
self.fetchThreadMessages(classroomId: classroomId, parentMessageId: docId1, docId: docId2)
} else {
print("Document does not exist")
}
}
}
}
}
return Message(id: docId1,
content: content,
name: displayName,
upvotes: likedUsersArr.count,
likedByUser: likedUsersArr.contains(self.user!.uid) ? true : false,
dontShow: dontShow,
sentAt: sentAt,
threadsArray: self.threadsTemp)
}
}
Another function: fetchThreadMessages:
// fetch a specified message and then append to the temporary threads array, threadsTemp
func fetchThreadMessages(classroomId: String, parentMessageId: String, docId: String) -> Message {
if (user != nil) {
let docRef = db.collection("chatrooms").document(classroomId).collection("messages").document(docId)
docRef.getDocument { (doc, error) in
if let doc = doc, doc.exists {
if let data = doc.data(){
let docId = doc.documentID
print("docid")
print(docId)
let content = data["content"] as? String ?? ""
let displayName = data["displayName"] as? String ?? ""
let likedUsersArr = data["likedUsers"] as? Array ?? [""]
// if message is in thread (but not last message), then don't show as normal message, but in thread
let dontShow = data["dontShow"] as? Bool ?? false
let sentAt = data["sentAt"] as? Date ?? Date()
self.threadsTemp.append(Message(id: docId,
content: content,
name: displayName,
upvotes: likedUsersArr.count,
likedByUser: likedUsersArr.contains(self.user!.uid) ? true : false,
dontShow: true,
sentAt: sentAt,
threadsArray: []))
}
}
}
}
}
I haven't implemented how the sendMessage() function updates the threadBefore array, but I'm currently updating this field directly on Firebase just for testing.
func sendMessage(messageContent: String, docId: String, collectionType: String, isReply: Bool, threadId: String) {
if (user != nil) {
if isReply {
let docRef = db.collection("chatrooms").document(docId).collection(collectionType).document(threadId)
self.threadRef = db.document(docRef.path)
}
db.collection("chatrooms").document(docId).collection(collectionType).addDocument(data: [
"sentAt": Date(),
"displayName": user!.email ?? "",
"content": messageContent,
"likedUsers": [String](),
"sender": user!.uid,
"threadBefore": isReply ? [threadRef] : [DocumentReference](),
"dontShow": false])
}
}
A little bit more on how I'm fetching and retrieving the document references from threadsBefore: For each message in the collection, I loop its threadsArray, which consists of DocumentReferences to other messesages that are in that thread. For each of those document references, I run self.fetchThreadMessages. This retrieves that message and stores a Message() instance in threadsTemp. Then, back in self.fetchMessage, when I'm done filling up the self.threadsTemp with all of the documents retrieved from threadsBefore, I store it in threadsArray property in the Message struct.
Now, look at the return state in self.fetchMessages above, the very last assignment inside Message() is threadsArray: self.threadsTemp. But the problem here is that this is just a reference? And it would change based on the last assignment to self.threadsTemp?
I've tried multiple ways to implement this entire storing and retrieving thing. But all came with several complicated errors. I tried using dictionaries, or storing just the document id's for the thread messages and then look them up in self.Messages (since it has all of the messages stored in it).
What's the best way to implement this? Or fix my errors?
I know my code is probably a mishmash of inefficient and confused coding practices. But I'm trying to learn.
With that being said (meaning, my comment above), I'm going to not as much address your code as I am going to propose what I think is a good way to organize your Firestore database for a chat app. Then, hopefully you will be able to apply it to your own situation.
First things first, we have to remember that Firestore charges based on the number of queries you make - it does not take into account the amount of data that is being fetched in a particular query - so fetching a single word is as expensive as fetching an entire novel. As such, we should structure our database in a way that is financially efficient.
What I would do personally is structure my database with two primary sections, (1) conversation threads and (2) users. Each user contains references to their threads they are a part of, and each thread contains a reference to the users that are in that thread. That way we can easily and efficiently fetch a user's conversations, or obtain the users in a particular conversation.
// COL = Collection
// DOC = Document
Threads (COL)
Thread1 (DOC)
id: String // id of the thread
participants: [String: String] = [
user1: [
"username": "Bob23",
"userID": IJ238KD... // the document id of the user
]
] // Keep this "participants" dictionary in the root of the thread
// document so we have easy access to fetch any user we want, if
// we so desire (like if the user clicks to view the profile of a
// user in the chat.
Messages (COL)
Message1 (DOC)
from: String // id of user that sent message
Message2 (DOC)
...
...
Thread 2 (DOC)
...
...
Users (COL)
User1 (DOC)
threadIDs: [String] // Contains the thread "id"s of all the threads
// the user is a part of. (You probably want to
// use a dictionary instead since array's can be
// difficult to work with in a database, but for
// simplicity, I'm going to use an array here.)
User2 (DOC)
...
...
Now, let's say the user opens the app and we need to fetch their conversation threads. It's as easy as
db.collection("Threads").whereField("id", in: user.threadIDs)
[EDIT] user.threads would be a local array of type String. You should have this data on hand since you would be fetching the current user's User document on app launch, which is why we include this data in that user document.
Basically, this query returns every document in the Threads collection whose "id" field exists in the array of the user's threadIDs (which is an array of type String). You can read more about Firestore queries in their docs here.
In other words, we get every thread document that the user has references to.
The great thing about this is that it takes only one query to return all of the conversations of a user.
Now let's say the user is scrolling through their conversation threads in the app, and they tap one of them to open up the messages. Now, all we do is fetch all the messages in that particular thread, again only requiring one query to do so.
Last but not least, if for some reason we have to get info about a particular user in a conversation, we have all the references we need within that thread document itself to fetch the user data, if needed.

How to merge two queries using Firestore - Swift

I need to merge two queries with firebase firestore and then order the results using the timestamp field of the documents.
Online I didn't find much information regarding Swift and Firestore.
This is what I did so far:
db.collection("Notes").whereField("fromUid", isEqualTo: currentUserUid as Any).whereField("toUid", isEqualTo: chatUserUid as Any).getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
return
}
db.collection("Notes").whereField("fromUid", isEqualTo: self.chatUserUid as Any).whereField("toUid", isEqualTo: self.currentUserUid as Any).getDocuments { (snaphot1, error1) in
if let err = error1{
print(err.localizedDescription)
return
}
}
}
I added the second query inside the first one on completion but now I don't know how to merge them and order them through the field of timestamp.
On this insightful question It is explained that it's recommended to use a Task object but I don't find anything similar with swift.
There are many ways to accomplish this; here's one option.
To provide an answer, we have to make a couple of additions; first, we need somewhere to store the data retrieved from firebase so here's a class to contains some chat information
class ChatClass {
var from = ""
var to = ""
var msg = ""
var timestamp = 0
convenience init(withDoc: DocumentSnapshot) {
self.init()
self.from = withDoc.get("from") as! String
self.to = withDoc.get("to") as! String
self.msg = withDoc.get("msg") as! String
self.timestamp = withDoc.get("timestamp") as! Int
}
}
then we need a class level array to store it so we can use it later - perhaps as a tableView dataSource
class ViewController: NSViewController {
var sortedChatArray = [ChatClass]()
The setup is we have two users, Jay and Cindy and we want to retrieve all of the chats between them and sort by timestamp (just an Int in this case).
Here's the code that reads in all of the chats from one user to another creates ChatClass objects and adds them to an array. When complete that array is passed back to the calling completion handler for further processing.
func chatQuery(from: String, to: String, completion: #escaping( [ChatClass] ) -> Void) {
let chatsColl = self.db.collection("chats") //self.db points to my Firestore
chatsColl.whereField("from", isEqualTo: from).whereField("to", isEqualTo: to).getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else { return }
var chatArray = [ChatClass]()
for doc in docs {
let chat = ChatClass(withDoc: doc)
chatArray.append(chat)
}
completion(chatArray)
})
}
Then the tricky bit. The code calls the above code which returns an array The above code is called again, returning another array. The arrays are combined, sorted and printed to console.
func buildChatArray() {
self.chatQuery(from: "Jay", to: "Cindy", completion: { jayCindyArray in
self.chatQuery(from: "Cindy", to: "Jay", completion: { cindyJayArray in
let unsortedArray = jayCindyArray + cindyJayArray
self.sortedChatArray = unsortedArray.sorted(by: { $0.timestamp < $1.timestamp })
for chat in self.sortedChatArray {
print(chat.timestamp, chat.from, chat.to, chat.msg)
}
})
})
}
and the output
ts: 2 from: Cindy to: Jay msg: Hey Jay, Sup.
ts: 3 from: Jay to: Cindy msg: Hi Cindy. Not much
ts: 9 from: Jay to: Cindy msg: Talk to you later

Struggling To Query Using getDocuments() in Firestore Swift

This is the first time I am using a Firestore Query and I'm struggling to parse the data. I normally use the same setup when I get documents (which works), but when I attach it to a query it does not work.
I am trying to query the database for the shop most visited, so I can later set it as favourite.
My Code:
func findFavouriteShop(completed: #escaping ([String]) -> Void)
{
// Variables
let dispatch = DispatchGroup()
var dummyDetails = [String]()
// References
let db = Firestore.firestore()
let userID = Auth.auth().currentUser?.uid
let groupCollectionRef = String("visits-" + userID! )
// Query the database for the document with the most counts
dispatch.enter()
db.collectionGroup(groupCollectionRef).order(by: "count", descending: true).limit(to: 1).getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching documents: \(err)")
}
else {
print(snapshot)
guard let snap = snapshot else {return}
for document in snap.documents {
let data = document.data()
// Start Assignments
let shopName = data["shopName"] as? String
let count = data["count"] as? String
// Append the dummy array
dummyDetails.append(shopName!)
dummyDetails.append(count!)
}
dispatch.leave()
}
dispatch.notify(queue: .main, execute: {
print("USER number of documents appended: \(dummyDetails.count)")
completed(dummyDetails)}
)
}
Using Print statements it seems as if the guard statement kicks the function out. The processor does not reach the for-loop to do the assignments. When I print the snapshot it returns an empty array.
I am sure I have used the wrong notation, but I'm just not sure where.
There's a lot to comment on, such as your choice of collection groups over collections (maybe that's what you need), why you limit the results to one document but feel the need to query a collection, the naming of your collections (seems odd), the query to get multiple shops but creating a function that only returns a single shop, using a string for a count property that should probably be an integer, and using a string array to return multiple components of a single shop instead of using a custom type.
That said, I think this should get you in the right direction. I've created a custom type to show you how I'd start this process but there's a lot more work to be done to get this where you need it to be. But this is a good starting point. Also, there was no need for a dispatch group since you weren't doing any additional async work in the document parsing.
class Shop {
let name: String // constant
var count: Int // variable
init(name: String, count: Int) {
self.name = name
self.count = count
}
}
func findFavouriteShops(completion: #escaping (_ shops: [Shop]?) -> Void) {
guard let userID = Auth.auth().currentUser?.uid else {
completion(nil)
return
}
var temp = [Shop]()
Firestore.firestore().collection("visits-\(userID)").order(by: "count", descending: true).limit(to: 1).getDocuments { (snapshot, error) in
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
completion(nil)
return
}
for doc in snapshot.documents {
if let name = doc.get("shopName") as? String,
let count = doc.get("count") as? String {
let shop = Shop(name: name, count: count)
temp.append(Shop)
}
}
completion(temp)
}
}
You can return a Result type in this completion handler but for this example I opted for an optional array of Shop types (just to demonstrate flexibility). If the method returns nil then there was an error, otherwise there are either shops in the array or there aren't. I also don't know if you're looking for a single shop or multiple shops because in some of your code it appeared you wanted one and in other parts of your code it appeared you wanted multiple.
findFavouriteShops { (shops) in
if let shops = shops {
if shops.isEmpty {
print("no error but no shops found")
} else {
print("shops found")
}
} else {
print("error")
}
}

How to call combine multiple completion handlers to combine data to one new array

I have been stuck for a while now and any advice would be greatly appreciated. I am creating an app that uses Firebase database and I have created 5 classes that hold different data in Firebase. I'm creating a tableview that needs to display information from each of the 5 classes (Profile name, image, then information about a league, and info about scores). So in my new class I created a function calling for data from firebase from each class...
For example: GET all players from X league {
FOR every player in the league {
GET the players information
THEN GET the scores
THEN on and on
once we have all information APPEND to new array
}
and then rank the array
}
After all this runs I want to reload the table view on the VC
SO my solution works on the original load but if I back out and re enter the screen the names and images repeat.
To be exact when the indexes print to the console I get
"Player 1: Zack"
"Player 2: John"
However, the screen shows John's image and name repeatedly. BUT only that class... All other data stays where it is supposed to be. And the original functions are all written the same way.
I'm thinking it's something to do with memory management or I wrote my completion handler poorly?
Here is the code in the new array class:
You'll also notice that my completion() is inside my for in loop which I HATE but it's the only way I could get the function to finish before completing.. Otherwise the function completes before the data is ready.
func getLeaderboard(leagueID: String, completion: #escaping ()->()) {
print("League Count After removeAll \(self.rankedGolfers.count)")
self.leagueMembers.getLeagueMembers(leagueID: leagueID) {
print("HANDLER: Step 1: Get League Members")
for member in self.leagueMembers.leagueMembers {
print("Golfer Member ID: \(member.userID)")
self.golferInfo.getGolferInfo(userKey: member.userID, completion: {
print("HANDLER: Step 2: Get player profile info")
print("Golfer Name3: \(self.golferInfo.golfers[0].firstName) \(self.golferInfo.golfers[0].lastName)")
self.handicapHelper.getRounds(userID: member.userID, completion: {
print("HANDLER: Step 3: Get players Handicap")
print("Golfer Handicap3: \(self.golferInfo.golfers[0].lastName): \(self.handicapHelper.handicap)")
self.leagueInfo.getLeagueInfo(leagueID: leagueID, completion: {
print("HANDLER: Step 4: Get league info")
let golferIndex = self.golferInfo.golfers[0]
let memberInfoIndex = self.leagueInfo.leagueInfo[0]
let golferID = member.userID
let profileImg = golferIndex.profileImage
let golferName = "\(golferIndex.firstName) \(golferIndex.lastName)"
let handicap = self.handicapHelper.handicap
let golferLeaguePardieScore = member.pardieScore
let leagueRoundsPlayed = member.numberOfRounds
let roundsRemaining = memberInfoIndex.leagueMinRounds - leagueRoundsPlayed
let currentWinnings = member.currentWinnings
let newGolfer = Leaderboard(golferID: golferID, profileImg: profileImg ?? "No Img", golferName: golferName, golferHandicap: handicap, golferLeaguePardieScore: golferLeaguePardieScore, roundsPlayedInLeague: leagueRoundsPlayed, roundsRemaining: roundsRemaining, currentWinnings: currentWinnings)
self.rankedGolfers.append(newGolfer)
print("HANDLER: Step 5: Add golfer to array")
//print("Golfer Name 4: \(newGolfer.golferName)")
//print("Rounds Remaining: \(newGolfer.roundsRemaining)")
print("league Member Count: \(self.rankedGolfers.count)")
self.getLeaderboardRanking()
print("HANDLER: Step 6: Rank Array")
//print("COMPLETION: \(self.rankedGolfers.count)")
completion()
})
})
})
}
}
}
Thank you for any help possible!
I think we can solve this with a DispatchGroup, which will ensure all of the data is loaded for each user, then append the user to an array used as a tableView dataSource and then reload the tableView upon completion.
To keep it simple we'll start with a UserInfo class which stores their uid, name, favorite food and handicap.
class UserInfoClass {
var uid = ""
var name = ""
var favFood = ""
var handicap = 0
}
and a class var array used as the dataSource for the tableView
var userInfoArray = [UserInfoClass]()
Then, assuming we have a structure like this...
users
uid_0
name: "Leroy"
handicaps
uid_0
amt: 4
fav_foods
uid_0
fav_food: "Pizza"
...here's a function that reads all users, then iterates over each one populating a UserInfoClass with their name and uid, as well as creating a dispatch group that also populates their favorite food and handicap. When that's complete the user is added to the dataSource array and when all of the users are read the tableView is reloaded to display the information.
func loadUsersInfoAndHandicap() {
let ref = self.ref.child("users")
self.userInfoArray = []
ref.observeSingleEvent(of: .value, with: { snapshot in
let group = DispatchGroup()
let allUsers = snapshot.children.allObjects as! [DataSnapshot]
for user in allUsers {
let uid = user.key
let name = user.childSnapshot(forPath: "name").value as? String ?? "No Name"
let aUser = UserInfoClass()
aUser.uid = uid
aUser.name = name
group.enter()
self.loadFavFood(withUid: uid) {favFood in
aUser.favFood = favFood
group.leave()
}
group.enter()
self.loadHandicap(withUid: uid) { handicap in
aUser.handicap = handicap
group.leave()
}
group.notify(queue: .main) {
self.userInfoArray.append(aUser)
}
}
group.notify(queue: .main) {
print("done, reload the tableview")
for user in self.userInfoArray {
print(user.uid, user.name, user.favFood, user.handicap)
}
}
})
}
the users name and uid is read from the main users node and here are the two functions that read their favorite food and handicap.
func loadFavFood(withUid: String, completion: #escaping(String) -> Void) {
let thisUser = self.ref.child("userInfo").child(withUid)
thisUser.observeSingleEvent(of: .value, with: { snapshot in
let food = snapshot.childSnapshot(forPath: "fav_food").value as? String ?? "No Fav Food"
completion(food)
})
}
func loadHandicap(withUid: String, completion: #escaping(Int) -> Void) {
let thisUser = self.ref.child("handicaps").child(withUid)
thisUser.observeSingleEvent(of: .value, with: { snapshot in
let handicap = snapshot.childSnapshot(forPath: "amt").value as? Int ?? 0
completion(handicap)
})
}
note that self.ref points to my firebase so substitute a reference to your firebase.
Note I typed this up very quickly and there is essentially no error checking so please add that accordingly.

How to make a function with a loop asynchronous in Swift?

I am creating an application for a library. I am trying to fetch all the books the user has checked out from Firebase, but my attempts to make the function asynchronous with a DispatchGroup doesn't seem to be working. I suspect this to be because of the for-in loop found inside of the function.
func fetchHistory() {
if items.count > 0 {
items.removeAll()
}
let myGroup = DispatchGroup()
myGroup.enter()
var itemNames = [String]() // this holds the names of the child values of /users/uid/items/ <-- located in Firebase Database
guard let uid = fAuth.currentUser?.uid else {return}
fData.child("users").child(uid).child("items").observe(.value, with: { snapshot in
// make sure there is at least ONE item in the history
if snapshot.childrenCount > 0 {
let values = snapshot.value as! NSDictionary
for i in values.allKeys {
itemNames.append(i as! String)
}
print(itemNames)
let uid = fAuth.currentUser!.uid // get the UID of the user
for item in itemNames {
fData.child("users").child(uid).child("items").child(item).observe(.value, with: { snapshot in
let values = snapshot.value as! NSDictionary
let bookTitle = values["title"] as! String
print(bookTitle)
let bookAuthor = values["author"] as! String
print(bookAuthor)
let bookCoverUrl = values["coverUrl"] as! String
print(bookCoverUrl)
let bookStatus = values["status"] as! String
print(bookStatus)
let bookDueDate = values["dueDate"] as! String
print(bookDueDate)
let book = Book(name: bookTitle, author: bookAuthor, coverUrl: bookCoverUrl, status: bookStatus, dueDate: bookDueDate)
self.items.append(book)
})
}
self.booksTable.isHidden = false
} else {
self.booksTable.isHidden = true
}
})
myGroup.leave()
myGroup.notify(queue: DispatchQueue.main, execute: {
self.booksTable.reloadData()
print("Reloading table")
})
}
Here is the output from the print() statements:
########0
Reloading table
["78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8"]
Mockingjay (The Hunger Games, #3)
Suzanne Collins
https://images.gr-assets.com/books/1358275419s/7260188.jpg
Checked
Replace
The first two lines of output should be printed AFTER everything else has printed. I really need some help on this, I have been stuck on this for hours. Thanks!
Edit:
As requested, here is my Firebase structure:
users:
meZGWn5vhzXpk5Gsh92NhSasUPx2:
ID: "12345"
firstname: "Faraaz"
items:
78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8
author: "Suzanne Collins"
coverUrl: "https://images.gr assets.com/books/1358275419s/..."
dueDate: "Date"
status: "Checked"
title: "Mockingjay (The Hunger Games, #3)"
type: "regular"
A couple of issues:
The pattern is that leave must be called inside the completion handler of the asynchronous call. You want this to be the last thing performed inside the closure, so you could add it as the the last line within completion handler closure.
Or I prefer to use a defer clause, so that not only do you know it will be the last thing performed in the closure, but also:
you ensure you leave even if you later add any "early exits" inside your closure; and
the enter and leave calls visually appear right next to each other in the code saving you from having to visually hunt down at the bottom of the closure to make sure it was called correctly.
You also, if you want to wait for the asynchronous calls in the for loop, have to add it there, too.
A very minor point, but you might want to not create the group until you successfully unwrapped uid. Why create the DispatchGroup if you could possibly return and not do any of the asynchronous code?
Thus, perhaps:
func fetchHistory() {
if items.count > 0 {
items.removeAll()
}
var itemNames = [String]()
guard let uid = fAuth.currentUser?.uid else {return}
let group = DispatchGroup()
group.enter()
fData.child("users").child(uid).child("items").observe(.value, with: { snapshot in
defer { group.leave() } // in case you add any early exits, this will safely capture
if snapshot.childrenCount > 0 {
...
for item in itemNames {
group.enter() // also enter before we do this secondary async call
fData.child("users").child(uid).child("items").child(item).observe(.value, with: { snapshot in
defer { group.leave() } // and, again, defer the `leave`
...
})
}
...
} else {
...
}
})
group.notify(queue: .main) {
self.booksTable.reloadData()
print("Reloading table")
}
}
While there is a brilliant answer from Rob, I would approach a solution from a different direction.
A book can only ever had one person check it out (at a time), but a borrower can have multiple books. Because of that relationship, simply combine who has the book with the book itself:
Here's a proposed users structure
users
uid_0
name: "Rob"
uid_1
name: "Bill"
and then the books node
books
78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8
author: "Suzanne Collins"
coverUrl: "https://images.gr assets.com/books/1358275419s/..."
dueDate: "Date"
status: "Checked"
title: "Mockingjay (The Hunger Games, #3)"
checked_out_by: "uid_0"
check_date: "20180118"
Then to get ALL of the books that Rob has checked out and use those results to populate an array and display it in a tableview becomes super simple:
//var bookArray = [Book]() //defined as a class var
let booksRef = self.ref.child("books")
let query = booksRef.queryOrdered(byChild: "checked_out_by").queryEqual(toValue: "uid_0")
booksRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let snap = child as! DataSnapshot
let book = Book(initWithSnap: snap) //take the fields from the snapshot and populate the book
self.bookArray.append(book)
}
self.tableView.reloadData()
})
But then you ask yourself, "self, what if I want a record of who checked out the book?"
If you need that functionality, just a slight change to the books node so we can leverage a deep query;
books
78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8
author: "Suzanne Collins"
coverUrl: "https://images.gr assets.com/books/1358275419s/..."
dueDate: "Date"
status: "Checked"
title: "Mockingjay (The Hunger Games, #3)"
check_out_history
"uid_0" : true
"uid_1" : true
and move the check out dates to the users node. Then you can query for any user of any book and have history of who checked out that book as well. (there would need to be logic to determine who has the book currently so this is just a starting point)
Or if you want another option, keep a separate book history node
book_history
78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8
-9j9jasd9jasjd4 //key is created with childByAutoId
uid: "uid_0"
check_out_date: "20180118"
check_in_date: "20180122"
condition: "excellent"
-Yuhuasijdijiji //key is created with childByAutoId
uid: "uid_1"
check_out_date: "20180123"
check_in_date: "20180125"
condition: "good"
The concept is to let Firebase do the work for you instead of iterating over arrays repeatedly and having to issue dozens of calls to get the data you need. Adjusting the structure makes it much simpler to maintain and expand in the future as well - and it avoids all of the issues with asynchronous code as it's all within the closure; nice and tidy.