How can I execute some code after fetching all childadded from Firebase in Swift 5?
I've tried using DispatchGroup and observe .value, but none of them worked efficiently.
let dispatchGroup = DispachGroup()
ref.child("path").observe(.childAdded, with: { (snapshot) in
self.dispatchGroup.enter()
//store snapshot data into an object
self.dispatchGroup.leave()
})
dispatchGroup.notify(queue: .main) {
//code to execute after all children are fetched
}
In this case, the code will be executed before fetching the data.
How can I execute code when only the callback block reaches the last child?
One option is to leverage that Firebase .value functions are called after .childAdded functions.
What this means is that .childAdded will iterate over all childNodes and then after the last childNode is read, any .value functions will be called.
Suppose we want to iterate over all users in a users node, print their name and after the last user name is printed, output a message that all users were read in.
Starting with a simple structure
users
uid_0
name: "Jim"
uid_1
name: "Spock"
uid_2
name: "Bones"
and then the code that reads the users in, one at a time, prints their name and then outputs to console when all names have been read
var initialRead = true
func readTheUsers() {
let usersRef = self.ref.child("users")
usersRef.observe(.childAdded, with: { snapshot in
let userName = snapshot.childSnapshot(forPath: "name").value as? String ?? "no name"
print(userName)
if self.initialRead == false {
print("a new user was added")
}
})
usersRef.observeSingleEvent(of: .value, with: { snapshot in
print("--inital load has completed and the last user was read--")
self.initialRead = false
})
}
and the output
Jim
Spock
Bones
--inital load has completed and the last user was read--
Note this will leave an observer on the users node so if a new user is added it will print their name as well.
Note Note: self.ref points to my root firebase reference.
When you're listening to .childAdded, there is no way when your code is getting called for the last child. So if you must treat the last child different, listen for .value and loop over the children as shown here.
Related
I have the following Firebase DB structure (see pic) which is created when a user sends a compliment to another user.
The compliment is saved under compliments with an auto-generated id
The compliment.key is also saved under users-compliments, under senderID/receiverID/then the "complimentID":"boolean" pair to indicate if it is "active"
I wish to check if an "active" compliment exists before being allowed to send another one to the same user.
But the following query results in a null output even though there is a child node with a value of 1.
What am I doing wrong here?
Query:
REF_USERS_COMPLIMENTS.child(currentUid).child(forId).queryEqual(toValue: "1").observeSingleEvent(of: .value) { (snapshot) in
print("snap: \(snapshot.value)")
}
Console output:
snap: Optional(<null>)
When using queryEqual() you have to combine it with an queryOrderedBy. In your case it would be queryOrderedByValue() because you want to compare the value:
REF_USERS_COMPLIMENTS.child(currentUid).child(forId).queryOrderedByValue().queryEqual(toValue: "1").observeSingleEvent(of: .value) { (snapshot) in
print("snap: \(snapshot.value)")
}
More information about this can be found in the docs.
Try this:
REF_USERS_COMPLIMENTS.child(currentUid).child(forId).observeSingleEvent(of: .value) { (snapshot) in
print("snap: \(snapshot.value)")
if let _ = snapshot.value as? NSNull {
// do your thing
}
}
My firebase database is structured as:
events
autoid
event name: ""
event date: ""
autoid
event name: ""
event date: ""
I currently have a function that returns all of the autoids from the events node then writes them to an array so I can use them in another snapshot.
The first time the function runs, it works as expected. But if I leave the view and come back it crashes. Which I think is because it's trying to append the array again, duplicating the values.
Here's my function
func getEvents() {
self.dispatchGroup.enter()
Database.database().reference().child("Events").observe(DataEventType.value, with: { (snapshot) in
if let dictionary = snapshot.children.allObjects as? [DataSnapshot] {
// self.dispatchGroup.enter()
for child in dictionary {
let eventid = child.key
self.eventsArray.append(eventid)
// print(eventid)
// print(self.eventsArray)
}
self.dispatchGroup.leave()
print(self.eventsArray)
}
})
}
Wondering how I can retrieve the existing autoids and any new ones that have been added when I return to the view. I tried .childadded but it returns event name, event date etc and I need the autoid.
I'm new to firebase and swift so any tips or recommendations are welcomed!
If you want to first handle the initial data and then get notified of only the new data, you're typically looking for the .childAdded event.
Database.database().reference().child("Events").observe(DataEventType.childAdded, with: { (snapshot) in
let eventid = snapshot.key
print(eventid)
self.eventsArray.append(eventid)
self.dispatchGroup.leave()
print(self.eventsArray)
}
When you first run this code, the .childAdded event fires for each existing child node. And after that, it fires whenever a new child is added. Similarly, you can listen for .childChanged and .childRemoved events to handle those.
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.
I am trying to load a username form a Firebase which is than supposed to be set in an Object. But the Firebase Observe Command is getting called after the name already gets set. What is the problem and how can I fix it?
let ref = Database.database().reference().child("Users").child(currentMessage.senderId).child("name")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
// This is supposed to be called first
self.username = snapshot.value as! String
print(self.username)
})
// This somehow gets called first
let nameModel = NameModel(name: self.username, uid: *some UID*)
decoratedItems.append(DecoratedChatItem(chatItem: nameModel, decorationAttributes: nil))
Firebase loads data from its database asynchronously. This means that the code executes in a different order from what you may expect. The easiest way to see this is with some log statements:
let ref = Database.database().reference().child("Users").child(currentMessage.senderId).child("name")
print("Before attaching observer")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
print("In completion handler")
})
print("After attaching observer")
Unlike what you may expect, this code prints:
Before attaching observer
After attaching observer
In completion handler
This happens because loading data from Firebase (or any other web service) may take some time. If the code would wait, it would be keeping your user from interacting with your application. So instead, Firebase loads the data in the background, and lets your code continue. Then when the data is available, it calls your completion handler.
The easiest way to get used to this paradigm is to reframe your problems. Instead of saying "first load the data, then add it to the list", frame your problem as "start loading the data. when the data is loaded, add it to the list".
In code this means that you move any code that needs the data into the completion handler:
let ref = Database.database().reference().child("Users").child(currentMessage.senderId).child("name")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
self.username = snapshot.value as! String
let nameModel = NameModel(name: self.username, uid: *some UID*)
decoratedItems.append(DecoratedChatItem(chatItem: nameModel, decorationAttributes: nil))
})
For some more questions on this topic, see:
Swift: Wait for Firebase to load before return a function
Can Swift return value from an async Void-returning block?
Array items are deleted after exiting 'while' loop?
Asynchronous functions and Firebase with Swift 3
I just want to ask about firebase retrieve data. How can i handle firebase retrieve data finished? I don't see any completion handler.
I want to call some function after this firebase data retrieve finished. How can i handle???
DataService.ds.POST_REF.queryOrderedByChild("created_at").observeEventType(.ChildAdded, withBlock: { snapshot in
if let postDict = snapshot.value as? Dictionary<String, AnyObject> {
let postKey = snapshot.key
let post = Post(postKey: postKey, dictionary: postDict)
self.posts.append(post)
}
})
In Firebase, there isn't really a concept of 'finished' (when listening to 'child added'). It is just a stream of data (imagine someone adds a new record before the initial data is 'finished'). You can use the 'value' event to get an entire object, but that won't give you new records as they're added like 'child added' does.
If, you really need to use child added and get notified when it's probably finished, you can set a timer. I don't know swift, but here's the logic.
Set up your 'child added' event.
Set a timer to call some finishedLoading() function in 500ms.
Each time the 'child added' event is triggered, destroy the timer set in step two and create another one (that is, extend it another 500ms).
When new data stops coming in, the timer will stop being extended and finsihedLoading() will be called 500ms later.
500ms is just a made up number, use whatever suits.
Do one request for SingleEventOfType(.Value). This will give you all info initially in one shot, allowing you to then do whatever function you want to complete once you have that data.
You can create a separate query for childAdded and then do anything there you want to do when a new post has been added
Write your entire block of code in a function which has a completion handler like so:
func aMethod(completion: (Bool) -> ()){
DataService.ds.POST_REF.queryOrderedByChild("created_at").observeEventType(.ChildAdded, withBlock: { snapshot in
if let postDict = snapshot.value as? Dictionary<String, AnyObject> {
let postKey = snapshot.key
let post = Post(postKey: postKey, dictionary: postDict)
self.posts.append(post)
}
completion(true)
})
}
Then call it somewhere like so:
aMethod { success in
guard success == true else {
//Do something if some error occured while retreiving data from firebase
return
}
//Do something if everything went well.
.
.
.