I am making a call to a firebase database with the following:
let myRef1 = Database.database().reference(withPath: "/tests/\ .
(myTest)")
print(myTest)
myRef1.observe(.value, with: {
snapshot in
print(snapshot)
var newItems: [qItem] = []
for item in snapshot.children {
let mItem = qItem(snapshot: item as! DataSnapshot)
newItems.append(mItem)
print(newItems)
}
self.myArray = newItems.shuffled()
print(self.myArray)
})
loadNext()
...
However it never completes the completion handler before it moves on to the next method call, which is dependent on the results from this.
Tried making it a separate method, etc. but nothing seems to work.
You need to add the method loadNext() inside the observe to be able to call it after retrieving the data. When you retrieve data it is happening asynchronously which means the compiler wont wait until all the data is retrieved, it will call the method loadNext() first and then after finishing retrieving the data it will execute print(newItems), so you need to do the following:
myRef1.observe(.value, with: {
snapshot in
print(snapshot)
var newItems: [qItem] = []
for item in snapshot.children {
let mItem = qItem(snapshot: item as! DataSnapshot)
newItems.append(mItem)
print(newItems)
loadNext()
}
self.myArray = newItems.shuffled()
print(self.myArray)
})
Firebase data is handled asynchronously. Doug Stevenson wrote a great blog about why that is and what it means for developers. I also wrote a blog where I show how to use closures in your functions to handle asynchronous data. While you definitely can call loadNext() from inside the observe closure, you may find that you end up with a long sequence of closures inside the same function. I go into this a little more in the blog post. In your specific case, you could do something like this:
func getArray(completion: #escaping ([qtItem]) -> Void) {
let myRef1 = Database.database().reference(withPath: "/tests/\ .
(myTest)")
print(myTest)
myRef1.observe(.value, with: {
snapshot in
print(snapshot)
var newItems: [qItem] = []
for item in snapshot.children {
let mItem = qItem(snapshot: item as! DataSnapshot)
newItems.append(mItem)
print(newItems)
}
self.myArray = newItems.shuffled()
print(self.myArray)
completion(self.myArray)
})
}
Then when you call getArray(), you can handle data from within the closure:
getArray() { newItems in
// do something with newItems, like pass to `loadNext()` if needed
self.loadNext()
}
Related
I'm trying to create a feed that should include posts made by people a user follows, and I'm using following function to get posts for user feed:
func observePosts(for userID: String, completion: #escaping ([UserPost], String) -> Void) {
let databaseKey = "Posts/\(userID)/"
databaseRef.child(databaseKey).queryOrderedByKey().queryLimited(toLast: 12).observe(.value) { snapshot in
var retrievedPosts = [UserPost]()
var lastSeenPostKey = ""
let dispatchGroup = DispatchGroup()
for snapshotChild in snapshot.children.reversed() {
guard let postSnapshot = snapshotChild as? DataSnapshot,
let postDictionary = postSnapshot.value as? [String: Any]
else {
print("Couldn't cast snapshot as DataSnapshot")
return
}
dispatchGroup.enter()
lastSeenPostKey = postSnapshot.key
do {
let jsonData = try JSONSerialization.data(withJSONObject: postDictionary as Any)
let decodedPost = try JSONDecoder().decode(UserPost.self, from: jsonData)
UserService.shared.observeUser(for: decodedPost.userID) { postAuthor in
decodedPost.author = postAuthor
retrievedPosts.append(decodedPost)
dispatchGroup.leave()
}
} catch {
print("Couldn't create UserPost from postDictionary")
}
}
dispatchGroup.notify(queue: .main) {
completion(retrievedPosts, lastSeenPostKey)
}
}
}
First step I'm getting user feed posts with .observe function and after that I'm iterating through every received post calling a UserService.shared.observeUser method that observes user data for every post to set post author's profile picture and username.
The problem with my method is that I'm using DispatchGroup's .enter and .leave methods to notify once every observe function got user data so that I could call completion block in which I'm reloading tableView.
Sure it works fine first time but since it's observe function it's called every time there's a change to user so once user updates his data (profile pic, usernama etc.) it crashes on dispatchGroup.leave() line since it's not balanced with dispatchGroup.enter()
What would be a correct approach to solve this issue?
I've been trying to solve this problem for 3 days searching all the internet, and only advice I found is to fetch it one time using observeSingleValue() instead of observing, but I really need to observe user profile data to keep profile pics and other necessary data up to date.
I'm trying to get my firebase database data into a variable to use it in my project
var someArray = [Array]()
let dbRef = Database.database().reference().child("SomeDatabase")
func loadSomeDatabaseData {
dbRef.observeSingleEvent(of: .value) { (snapshot) in
let someDict = snapshot.value as! [String:Any]
let keysOfSomeDict = Array(someDict.keys)
self.someArray.append(contentsOf: keysOfSomeDict)
self.collectionView?.reloadData()
}
}
I've tried calling loadSomeDatabaseData() in my viewDidload, followed by printing someArray, which results in an empty array. I know the keysOfSomeDict array has the correct data that I want, since i tried printing this array directly inside the closure. I would however also like to be able to print and use this data elsewhere in my app.
The Firebase observeSingleEvent method is asynchronous method. it executive in background only because it take time to fetch data from Firebase.
if you print array immediately means you get only empty array.
so print array once you get the data from Firebase. for that you can use escaping closure
Function Declaration:
func loadSomeDatabaseData(resultArray : #escaping([Array])->()) {
dbRef.observeSingleEvent(of: .value) { (snapshot) in
let someDict = snapshot.value as! [String:Any]
let keysOfSomeDict = Array(someDict.keys)
self.someArray.append(contentsOf: keysOfSomeDict)
resultArray(self.someArray)
}
}
Func Call:
self.loadSomeDatabaseData{(firebaseReposne) in
print("FirebaseData" , firebaseReposne) // Hope here you will get your firebase data.
self.collectionView?.reloadData()
}
The observeSingleEvent is asynchronous. So immediately printing someArray after calling loadSomeDatabaseData will result in an empty array. It takes sometime to retrieve the data from Firebase api.
To use this data elsewhere in the app, you could set a flag indicating the data is loaded or send a notification to inform the data is available.
I'm trying to get data from my database and after obtaining a piece of data, using that piece of data to find a new piece of data.
At the end, I can piece these together and return the derived data. I'm not sure this is the best way to approach this, but this is where im at as of now.
My problem is that each call to the database (Firebase) is async and therefore I need to somehow wait for the async to finish, before going on.
I've looked at dispatch group and heres what I have so far:
let taskGroup = DispatchGroup()
for buildingKey in building.allKeys
{
var aprt = NSDictionary()
taskGroup.enter()
// ASYNC REQUEST
getAbodesWithUID(UID: buildingKey as! String, callback: { (success, abodes) in
aprt = abodes
taskGroup.leave()
})
taskGroup.enter()
for key in aprt.allKeys
{
// ASYNC REQUEST
getTenantsWithAprt(UID: key as! String, callback: { (success, user) in
for userKey in user.allKeys
{
let dict = NSMutableDictionary()
dict.setValue(((building[buildingKey] as? NSDictionary)?["Address"] as? NSDictionary)?.allKeys[0] as? String, forKey: "Building")
dict.setValue((user[userKey] as? NSDictionary)?["Aprt"], forKey: "Number")
dict.setValue((user[userKey] as? NSDictionary)?["Name"], forKey: "Name")
dict.setValue(userKey, forKey: "UID")
dict.setValue((user[userKey] as? NSDictionary)?["PhoneNumber"], forKey: "Phone")
apartments.append(dict)
}
taskGroup.leave()
})
}
}
taskGroup.notify(queue: DispatchQueue.main, execute: {
print("DONE")
callback(true, apartments)
})
I can't seem to get it to callback properly
First, you should be iterating over aprt.allKeys inside of the callback for getAbodesWithUID, other wise, when the for loop executes aprt will be an empty dictionary.
Secondly, the taskGroup.enter() call above that for loop should be inside of the for loop, because it needs to be called once for every key. It should be placed where the // ASYNC REQUEST comment currently is.
This is precisely what "promises" are for is for. They are available in Swift via a 3rd party add-in. A popular way to do this is to push all your reads/gets into an array. Then you promise.all(yourArray) which returns the array of results/values that you then iterate over to get at each one.
From this other answer:
You can look into when which may provide what you need and is covered
here.
Use the loop to put your promises into an array and then do something
like this:
when(fulfilled: promiseArray).then { results in
// Do something
}.catch { error in
// Handle error
}
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.
So this is my Firebase Structure:
I'm trying to get all books pictures (bookImage), add them to list and then use this list to fill a table or anythings else. (I'm using swift 3)
struct item {
let picture: String!}
var items = [item]()
func getLatestAddedItems(){
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("Items").observe(.childAdded, with: {
FIRDataSnapshot in
let picture = (FIRDataSnapshot.value as? NSDictionary)?["bookImage"] as? String ?? ""
//self.items.insert(item(picture: picture), at: 0)
self.items.append(item(picture: picture))
print(self.items[0].picture)
print(self.items[1].picture) // error here
})}
I'm able to see the first print output but on the second one I'm getting fatal error: Index out of range even I have 3 books on my database.
Since your using .childAdded, it iterates through that closure for each object in the data tree, in this case, each book. When you try to print the second picture, its still in its first iteration. Meaning you only have retrieved the first book so far. That's why you can print the first book item but not the second one. If you moved the print statements outside of the closure, and then did the print statements after the closure iterated over all three books, you wouldn't get the error.
Don't change it to .value unless if every time a new one is subsequently added you want to get the entire list of books all over again. If its a large amount of books, it will be a lot of data to go through each time.
Summary: .childAdded gives you one book at a time, with a new snapshot for each one. .value gives you all the books in one snapshot, then you must iterate over them yourself in the closure. ex.
for snap in snapshot.children {
// now you can do something with each individual item
}
also I just noticed your using the FIRDataSnapshot type in your closure, that should be a variable which represents the snapshot you received, not the type itself. Change "FIRDataSnapshot in" to something like "snapshot in" snapshot is a representation of what information was given to you by the observe closure, in this case, an object with a type of FIRDataSnapshot.
Edit:
Your solution you mentioned below works fine, but I'll add an alternative that is cleaner and easier to use.
add an init method to your Book class that takes a FIRDataSnapshot as the init parameter, then init the object when you query Firebase:
struct Book {
let bookImageString: String
init?(snapshot: FIRDataSnapshot) {
guard let snap = snapshot.value as? [String : AnyObject], let urlString = snap["bookImage"] else { return nil }
bookImageString = imageString
{
{
then when you query firebase you can do this:
for snap in snapshot.children {
if let snap = snap as? FIRDataSnapshot, let book = Book(snapshot: snap) {
self.items.append(book)
{
}
doing it this way cleans up the code a little bit and leaves less chance of error in the code.
Also, since your using .value, make sure to empty the data source array at the beginning of the closer, or else you will get duplicates when new books are added.
items.removeAll()
Finally I'm posting the solution:
func getLatestAddedItems(){
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("Items").observe(.value, with: {
snapshot in
//self.items.insert(item(picture: picture), at: 0)
for childSnap in snapshot.children.allObjects {
let snap = childSnap as! FIRDataSnapshot
print(snap.key)
let picture = (snap.value as? NSDictionary)?["bookImage"] as? String ?? ""
print(picture)
}
})
}