Trouble understanding array appending - ios

func EmptySearchBarList(){
self.PersonalSearchesList = []
currentUserFirebaseReference.child("rooms").observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
print("snap")
print(self.PersonalSearchesList.count) // Calling 0 every single time
DataService.ds.REF_INTERESTS.child(interestKey).observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
if snapshot.value != nil {
if let users = (snapshot.value! as? NSDictionary)?["users"] as? Dictionary<String,AnyObject> {
DataService.ds.REF_USERS.child(topUser).child("pictureURL").observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
self.PersonalSearchesList.append(eachSearch)
print("first one")
print(self.PersonalSearchesList.count)
})
}
}
})
}
print("Second one")
print(self.PersonalSearchesList.count)
print("Calling to set my sorted PersonalSearchesList")
self.mySortedList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
}
}
initialLoadOver = true
}
What code I'm trying to ultimately run is this :
var mySortedList = [Interest](){
didSet{
print("this was called")
let myCount = mySortedList.count
print(self.PersonalSearchesList.count)
print(myCount)
self.tableView.reloadData()
}
}
The attempt is to load up my PersonalSearchesList array, and once the snap in snapshots is done running, I'm setting MySortedList equal to PersonalSearchesList and reloading the tableview.
What I don't understand is why the prints are coming out like they are. The snap/ 0's are coming from the top of my for snap in snapshots. It seems like it should instead be snap / 1 , snap / 2, snap / 3.
The code to be called when the snaps are done is correct in the timeline, once the snaps have gone through that code runs. What doesn't make sense is why it's not until after that the items are actually being appended to PersonalSearchesList. Becuase of how everything is Im' setting my filtered array to an empty personal searches array and then afterwards I'm filling it up.
Any ideas here?
edit:
var dispatchGroup = DispatchGroup()
dispatchGroup.enter()
dispatchGroup.leave()
dispatchGroup.notify(queue: DispatchQueue.global(), execute: {
})

DataService.ds.REF_INTERESTS.child(interestKey).observeSingleEvent is running asynchronous so all of those callbacks (where you actually fill you list) will run when they are called from the DataService.
You could use a dispatch group to do what you want.
http://commandshift.co.uk/blog/2014/03/19/using-dispatch-groups-to-wait-for-multiple-web-services/
func EmptySearchBarList(){
var dispatchGroup = DispatchGroup()
self.PersonalSearchesList = []
currentUserFirebaseReference.child("rooms").observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
dispatchGroup.enter()
single time
DataService.ds.REF_INTERESTS.child(interestKey).observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
if snapshot.value != nil {
if let users = (snapshot.value! as? NSDictionary)?["users"] as? Dictionary<String,AnyObject> {
DataService.ds.REF_USERS.child(topUser).child("pictureURL").observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
self.PersonalSearchesList.append(eachSearch)
print("snap")
print(self.PersonalSearchesList.count)
})
}
}
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: DispatchQueue.global(), execute: {
print("Second one")
print(self.PersonalSearchesList.count)
print("Calling to set my sorted PersonalSearchesList")
self.mySortedList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
})
}
}
initialLoadOver = true
}
I'm not completely sure you can enter and leave the group several times from the same thread so you might have to wrap the code from enter until the end of the callback in another thread and call it async

Related

FirebaseDatabase -How to Paginate when using a UISearchController

As the user types text into the searchBar the UISearchController has a delegate method to update search results:
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text?.lowercased() else { return }
Database...usersRef
.queryOrdered(byChild: "username")
.queryStarting(atValue: searchText)
.queryEnding(atValue: searchText+"\u{f8ff}")
.observe( .childAdded, with: { [weak self](snapshot) in
let key = snapshot.key
guard let dict = snapshot.value as? [String: Any] else { return }
let user = User(userId: key, dict: dict)
self?.datasource.append(user)
})
}
That works fine.
When I normally paginate I use this procedure:
var startKey: String?
func handlePaginationForPosts() {
if startKey == nil {
Database...PostsRef
.queryOrderedByKey()
.queryLimited(toLast: 10)
.observeSingleEvent(of: .value, with: { [weak self] (snapshot) in
guard let children = snapshot.children.allObjects.first as? DataSnapshot else { return }
if snapshot.childrenCount > 0 {
for child in snapshot.children.allObjects as! [DataSnapshot] {
let postId = child.key
if child.key != self?.startKey {
guard let dict = child.value as? [String:Any] else { return }
let post = Post(postId: postId, dict: dict)
self?.datasource.insert(post, at: 0)
}
}
self?.startKey = children.key
}
})
} else {
let lastIndex = datasource.count
Database...PostsRef
.queryOrderedByKey()
.queryEnding(atValue: startKey!)
.queryLimited(toLast: 11)
.observeSingleEvent(of: .value, with: { [weak self] (snapshot) in
guard let children = snapshot.children.allObjects.first as? DataSnapshot else { return }
if snapshot.childrenCount > 0 {
for child in snapshot.children.allObjects as! [DataSnapshot] {
let postId = child.key
if child.key != self?.startKey {
guard let dict = child.value as? [String:Any] else { return }
let post = Post(postId: postId, dict: dict)
// I run a check to make sure the datasource doesn't contain the post before adding it
self?.datasource.insert(post, at: lastIndex)
}
}
self?.startKey = children.key
}
})
}
}
The problem here is when running a search I use:
.queryStarting(atValue: searchText)
.queryEnding(atValue: searchText+"\u{f8ff}")
But when paginating a post I use:
.queryOrderedByKey()
.queryEnding(atValue: startKey!) ...
self?.datasource.insert(post, at: lastIndex)
The startKey is the first key in the snapshot.children.allObjects.first and the lastIndex is the datasource.count.
Considering the search query is based on the search text and not a key, how can I paginate when I'm already using .queryEnding(atValue: searchText+"\u{f8ff}") instead of .queryEnding(atValue: startKey!)?
I need to track the key that was pulled from the db so that when I paginate I can run the next set of results from that particular key.
Firebase Database queries can only order/filter on a single property.
So what you can do is filter for the search criteria, and then limit to the firsts N results.
What you can't do is filter for the search criteria, skip the first N results, and get the next page.
The closest you can get, and something regularly done for cases such as this, is retrieve the first 2*N results when you need to show page 2. This wastes some bandwidth though, so you'll have to trade that off against how useful the pagination is.

iOS Firebase asynchronous data retrieval into Array

I try to retrieve data from Firebase into Array. Because it runs asynchronously, the results that I want to show in my CollectionView is a delay until I switch back and forth. I am very new to asynchronous functions in iOS. Please help me to complete my code.
ref = Database.database().reference(withPath: "MyTest/Video")
ref?.observeSingleEvent(of: .value, with: { snapshot in
if !snapshot.exists() { return }
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let autoID = child.key as String //get autoID
let title = snapshot.childSnapshot(forPath: "\(autoID)/Title").value
let url = snapshot.childSnapshot(forPath: "\(autoID)/URL").value
let views = snapshot.childSnapshot(forPath: "\(autoID)/Views").value
self.arrayAllTitle.append(title as! String)
self.arrayAllId.append(url as! String)
self.arrayAllDesc.append(views as! String)
}
}
})
You need to reload the collection after you retrieve the data so after the for loop call reloadData()
for child in result {
}
self.collectionView.reloadData()
//
func getValueFromDatabase(completion: #escaping (_ status: Bool) -> Void){
ref = Database.database().reference(withPath: "MyTest/Video")
ref?.observeSingleEvent(of: .value, with: { snapshot in
if !snapshot.exists() { return }
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let autoID = child.key as String //get autoID
let title = snapshot.childSnapshot(forPath: "\(autoID)/Title").value
let url = snapshot.childSnapshot(forPath: "\(autoID)/URL").value
let views = snapshot.childSnapshot(forPath: "\(autoID)/Views").value
self.arrayAllTitle.append(title as! String)
self.arrayAllId.append(url as! String)
self.arrayAllDesc.append(views as! String)
}
completion(true)
}
else {
completion(false)
}
})
}
//
self.getValueFromDatabase { (status) in
if status {
// success
}
}
I'm working with Firebase in my project right now. I would suggest the following solution: wrap the database observer in a distinct function which gets completion block as a parameter.
func getValueFromDatabase(completion: ()->Void){
ref = Database.database().reference(withPath: "MyTest/Video")
ref?.observeSingleEvent(of: .value, with: { snapshot in
if !snapshot.exists() { return }
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let autoID = child.key as String //get autoID
let title = snapshot.childSnapshot(forPath: "\(autoID)/Title").value
let url = snapshot.childSnapshot(forPath: "\(autoID)/URL").value
let views = snapshot.childSnapshot(forPath: "\(autoID)/Views").value
self.arrayAllTitle.append(title as! String)
self.arrayAllId.append(url as! String)
self.arrayAllDesc.append(views as! String)
}
completion()
}
})
}
This way you can call the function from anywhere providing the desired action after fetching data from db is finished:
getValueFromDatabase(completion:{
self.collectionView.reloadData() //or any other action you want to fulfil
})

How do I observe the values of my Firebase database and use a closure?

How do I asynchronously observe a Firebase node? Right now, I'm using a DispatchGroup to fire off a closure once all the children in a node are observed.
I want to keep a listener on this node, because I expect the values to change in the future.
If I use a dispatchGroup, an update in the database causes the dispatchGroup.leave() to fire, without a corresponding .enter() causing my app to crash. Here is my code:
func fetchPosts(completion: #escaping ([Post]) -> Swift.Void) {
let dispatchGroup = DispatchGroup()
guard let uid = Auth.auth().currentUser?.uid else { return }
let dbRef = Database.database().reference()
let userPosts = dbRef.child("user-posts")
let postRef = dbRef.child("posts")
userPosts.child(uid).observe(.value, with: { (snap) in
if let dictionary = snap.value as? [String:Any] {
for item in dictionary {
dispatchGroup.enter()
postRef.child(item.key).observe(.value, with: { (snap) in
if let dictionary = snap.value as? [String:Any] {
let newPost = Post(dictionary: dictionary)
self.posts.append(newPost)
dispatchGroup.leave()
}
})
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion(self.posts)
}
})
}

How Do I Stop The Network Indicator to Stop Spinning when there is nothing from firebase to grab

I have a function which is called when the view is loaded and when the user pulls down the table view to refresh the view. This function takes a snapshot and then puts the info on the tableView. I have a network activity indicator that is used. It works correctly when there is information to grab and He indicator stops spinning once the data has been fetched, but if there is no data to grab then it just keeps spinning. I want to make it stop spinning if there is no data to grab.
ref.observe(.childAdded, with: { (snapshot) in
let userId = snapshot.key
print(snapshot.childrenCount)
//print("This is interesting...", snapshot.value)
print(userId)
if userId == uid {
let bookRef = FIRDatabase.database().reference().child("user-books").child(userId)
bookRef.observe(.childAdded, with: { (snapshot) in
let bookID = snapshot.key
print("This is new",snapshot.children)
print(bookID,"sigh..")
self.bookKey = snapshot.key
let booksIDref = FIRDatabase.database().reference().child("books").child(bookID)
booksIDref.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject]{
let book = Book()
book.setValuesForKeys(dictionary)
self.books.append(book)
print(book.Author)
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}, withCancel: nil)
}, withCancel: nil) }
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}, withCancel: nil)
}
I found out the correct way to do it with FrankvanPuffelen's help. I used snapshot.exists () I put it in the second snapshot since that is the one that would be empty if there where no items stored there. I used an if statement as a check and now it works excellently.
ref.observe(.childAdded, with: { (snapshot) in
let userId = snapshot.key
print(snapshot.childrenCount)
print(userId)
if userId == uid {
let bookRef = FIRDatabase.database().reference().child("user-books").child(userId)
bookRef.observe(.childAdded, with: { (snapshot) in
let bookID = snapshot.key
print("This is new",snapshot.children)
self.bookKey = snapshot.key
let booksIDref = FIRDatabase.database().reference().child("books").child(bookID)
booksIDref.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject]{
let book = Book()
book.setValuesForKeys(dictionary)
self.books.append(book)
print(book.Author)
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}, withCancel: nil)
if snapshot.exists(){
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}
}, withCancel: nil) }
}, withCancel: nil)

Firebase Query Running At Wrong Time

For some reason when I run the following method, the first query runs after the method completes. I tried using a dispatch block in order to force the query to run first, but the query never runs at all then and the app simply freezes. Let me know if you know what is wrong.
Method without dispatch group:
func loadConversations() {
let ref = FIRDatabase.database().reference()
let convoRef = ref.child("users").child(FIRAuth.auth()!.currentUser!.uid).child("conversations")
var conversationID = [String]()
print(1)
convoRef.queryOrderedByKey().observeSingleEvent(of: .value, with: { (snapshot) in
let enumerator = snapshot.children
print(2)
while let rest = enumerator.nextObject() as? FIRDataSnapshot {
print(3)
if let id = rest.value as? String{
conversationID.append(id)
print(id)
}
}
})
print(4)
print("size: \(conversationID.count)")
for id in conversationID {
print(5)
ref.child("conversations").queryEqual(toValue: id).observeSingleEvent(of: .value, with: { (snapshot) in
print(6)
if let convo = snapshot.value as? [String : AnyObject] {
print(7)
let conversation = Conversation()
conversation.conversationID = id
conversation.name = "Temporary test name"
self.Conversations.append(conversation)
}
})
ref.removeAllObservers()
}
print(8)
self.conversationTableView.reloadData()
ref.removeAllObservers()
}
This prints:
1
4
size: 0
8
2
3
-KZyMMzXmkQC_OF0T08_
With the dispatch group:
func loadConversations() {
let dispatchGroup = DispatchGroup()
let ref = FIRDatabase.database().reference()
let convoRef = ref.child("users").child(FIRAuth.auth()!.currentUser!.uid).child("conversations")
var conversationID = [String]()
print(1)
dispatchGroup.enter()
convoRef.queryOrderedByKey().observeSingleEvent(of: .value, with: { (snapshot) in
let enumerator = snapshot.children
print(2)
while let rest = enumerator.nextObject() as? FIRDataSnapshot {
print(3)
if let id = rest.value as? String{
conversationID.append(id)
print(id)
dispatchGroup.leave()
}
}
})
print(4)
dispatchGroup.wait()
print("size: \(conversationID.count)")
for id in conversationID {
print(5)
ref.child("conversations").queryEqual(toValue: id).observeSingleEvent(of: .value, with: { (snapshot) in
print(6)
if let convo = snapshot.value as? [String : AnyObject] {
print(7)
let conversation = Conversation()
conversation.conversationID = id
conversation.name = "Temporary test name"
self.Conversations.append(conversation)
}
})
}
print(8)
self.conversationTableView.reloadData()
ref.removeAllObservers()
}
This prints
1
4
but then it just freezes and waits. The query never runs.
I am not sure why the query just does not appear to be entered. When the query is entered, it works perfectly fine, but it is entered too late. Any help is greatly appreciated. Thanks!
This is simply because Firebase queries are executed on a background thread as it is essentially a network call. Hence the response comes after your method completes, otherwise the UI will be blocked until a response comes from Firebase
You need to write a closure inside your query response to execute a block of code as soon as you get the response.
func loadConversations(completion:#escaping (Array<String>) -> Void) -> Void {
let ref = FIRDatabase.database().reference()
let convoRef = ref.child("users").child(FIRAuth.auth()!.currentUser!.uid).child("conversations")
var conversationID = [String]()
print(1)
convoRef.queryOrderedByKey().observeSingleEvent(of: .value, with: { (snapshot) in
let enumerator = snapshot.children
print(2)
while let rest = enumerator.nextObject() as? FIRDataSnapshot {
print(3)
if let id = rest.value as? String{
conversationID.append(id)
print(id)
}
}
completion(conversationID)
})
}
This will send your call back to wherever it was called from and inside
loadConversations { (array) in
//do something with this value and execute next query
}

Resources