Database observer function not acting as observer in viewDidLoad, only viewDidAppear - ios

I have a function that is in charge of observing the database. It sets some dictionaries upon first load, and then keeps an eye on changes thereafter and updates those dictionaries accordingly. When called in viewDidAppear, this works perfectly. But if I move it to viewDidLoad, it sets the values initially, but doesn't "observe" - in other words if I change one of the values, for example status, that change is not reflected until I leave the view and come back.
I need to have it in viewDidLoad for other reasons - why exactly is it only properly working as an observer if it's in viewDidAppear, and is there anything I can change to make it work as an observer in viewDidLoad?
This is the function:
func getParticipantInfo() {
let databaseRef = FIRDatabase.database().reference()
let groupRef = databaseRef.child("groups").child(currentRoomIdGlobal)
groupRef.observe(.childAdded, with: { snapshot in
if let snapDict = snapshot.value as? [String : AnyObject] {
for each in snapDict {
let uid = each.key
let avatar = each.value["profilePicture"] as! String
let gender = each.value["gender"] as! String
let handle = each.value["handle"] as! String
let name = each.value["name"] as! String
let status = each.value["status"] as! String
// Set those to the dictionaries [UID : value]
self.avatarDictionary.setValue(avatar, forKey: uid)
self.nameDictionary.setValue(name, forKey: uid)
self.genderDictionary.setValue(gender, forKey: uid)
self.handleDictionary.setValue(handle, forKey: uid)
self.statusDictionary.setValue(status, forKey: uid)
DispatchQueue.main.async {
// Nav bar
self.navCollectionView?.collectionView?.reloadData()
}
}
}
})
}

One reason I can see is that you are observing only .childAdded events. You'll have to subscribe to all the changes in order to trigger the event. try subscribing to .value event which will listen to all the changes on Database and reflect in app.
As far as viewDidLoad and viewDidAppear is concerned, make sure that you have setup all the class variables before attaching a subscriber to Firebase. viewDidLoad or viewDidAppear doesn't matter as far as the setup is right.

Related

Getting conditionals to work within Firebase Observer

I am trying to do a conditional within Firebase Observer.
Essentially I like to check if seat is occupied.
If it is, then orders can be retrieved.
If not then send the restaurant back to the search seat page again.
For some reason, the code within if !taken is never executed even if the condition is met (ie. the owner has inputted the wrong seat number). I have put it within the closure, it should run right?
func retrieveData (){
var taken = false
var seatNumber = "**an Int from other page**"
let refCustomer = Database.database().reference().child("Restaurant").child("Customers")
refCustomer.queryOrdered(byChild: "Seat").queryEqual(toValue: "\(seatNumber)").observeSingleEvent(of: .childAdded, with: { (snapshot) in
if snapshot.exists() {
taken = true
let snapshotValue = snapshot.value as? [String : AnyObject] ?? [:]
self.customerFirstName = snapshotValue["Firstname"] as! String
self.customerLastName = snapshotValue["Lastname"] as! String
self.customerAllergy = snapshotValue["Allergy"] as! String
self.customerID = snapshot.key
self.allergy.text = self.customerAllergy
self.ptname.text = "\(self.customerFirstName) \(self.customerLastName)"
}
if !taken {
print ("oops")
self.performSegue(withIdentifier: "MainPage", sender: self)
}
})
}
There are a number of issues with this code, and possibly your structure so let me see if I can point you in the right direction.
First, you can eliminate the taken varable as it's unneeded. In short
if snapshot.exists() {
//handle the snapshot
} else { //use else here as if the snapshot doesn't exist we want this code to run
print ("oops")
}
Second, ensure your structure is like this
Customers
cust_0
Seat: 1
cust_1
Seat: 2
etc
Third, this is a string of "1"
queryEqual(toValue: "\(seatNumber)")
and you want to query for an Int so make it
queryEqual(toValue: seatNumber)
which queries for an Int of 1
Forth:
When querying Firebase, closures will not execute when .childAdded doesn't find anything. You should use .value.
From the docs of .childAdded
This event is triggered once for each existing child and then again
every time a new child is added to the specified path.
So if no child nodes match the query, it will not execute.
Use this
refCustomer.queryOrdered(byChild: "Seat")
.queryEqual(toValue: seatNumber)
.observeSingleEvent(of: .value, with: { (snapshot) in
And... this is the important part, .value retrieves all nodes that match the query so you will need to iterate over those nodes to work with the child nodes. Assuming there would only ever be one match then you can do this
guard let allChildren = snapshot.children.allObjects as? [DataSnapshot] else {return}
let firstChild = allChildren.first
Fifth. While technically this is ok
let f = snapshotValue["Firstname"] as! String
You are guaranteeing that a Firstname node always exists. If that's true go with it. However, a safer, more Swifty way would be to do this
let f = snapshotValue["Firstname"] as? String ?? "No First Name"

(Swift + Firebase) Convert observeSingleEvent to observe .childAdded

I have a observeSingleEvent in my viewDidLoad function and I want to change it into a observe with .childAdded so that it will listen constantly and add the objects whenever they get added to the database.
Here's the code that reads from firebase:
let parentRef = Database.database().reference().child("Recipes")
parentRef.observeSingleEvent(of: .value, with: { snapshot in
// PROCESSES VALUES RECEIVED FROM SERVER
if ( snapshot.value is NSNull ) {
// DATA WAS NOT FOUND
print("– – – Data was not found – – –")
} else {
// DATA WAS FOUND
for user_child in (snapshot.children) {
let user_snap = user_child as! DataSnapshot
let dict = user_snap.value as! [String: String?]
// DEFINE VARIABLES FOR LABELS
let recipeName = dict["Name"] as? String
let recipeDescription = dict["Description"] as? String
food.append(Element(name: recipeName!, description: recipeDescription!))
self.tableView.reloadData()
}
}
})
Another problem that I have now (besides that it only loads once) is that whenever I go to another view in the app and then come back it reads everything again so I get duplicates of everything in my tableView, will that still happen with the observe?
Edit:
Here is what my database looks like:
Recipes
-Kv7FAqgLtDrRoyGd-99
Description: "food description"
Name: "food name"
-KvBuzMUnIQXn8gpG2WL
Description: "food description2"
Name: "food name2"
-KvH6yYeJaThK7oP8xBj
Description: "food description3"
Name: "food name3"
Change observeSingleEvent to observe.
Empty food array whenever observing new values.
Reload your food array outside your for-loop so that you only reload whenever you have loaded all of your items into your array.
let parentRef = Database.database().reference().child("Recipes")
// 1. Change to observe
parentRef.observe(.value, with: { snapshot in
// PROCESSES VALUES RECEIVED FROM SERVER
if ( snapshot.value is NSNull ) {
// DATA WAS NOT FOUND
print("– – – Data was not found – – –")
} else {
// 2. Empty food array
self.food = []
// DATA WAS FOUND
for user_child in (snapshot.children) {
let user_snap = user_child as! DataSnapshot
let dict = user_snap.value as! [String: String?]
// DEFINE VARIABLES FOR LABELS
let recipeName = dict["Name"] as? String
let recipeDescription = dict["Description"] as? String
food.append(Element(name: recipeName!, description: recipeDescription!))
}
// 3. reload tableview outside loop
self.tableView.reloadData()
}
})
By emptying your food array whenever observing new values you ensure to only show the values that are present in your database and thus never having repetitive elements.

How can I display users nickname from Firebase in an array then in a TableView (For loop issue)

I'm developing an app when I'm trying to find users by retrieving them nickname from Firebase Database during search in a search bar. I want put all my users in an array at first but I can't.
Edit with the code working :
var userTab = [String]()
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
ref.child("users").queryOrdered(byChild: "pseudo").observeSingleEvent(of: .value, with: { (snapshot) in
if (snapshot.value is NSNull) {
print("Aucune donnée trouvée dans la base de données")
} else {
for child in snapshot.children {
let userSnap = child as! DataSnapshot
let uid = userSnap.key
let userDict = userSnap.value as! [String: AnyObject]
let pseudo = userDict["nickname"] as! String
let total = snapshot.childrenCount
print(pseudo)
self.userTab.append(pseudo) // I change this line self.userTab = [pseudo]
self.resultsController.tableView.reloadData() // I add this line
print(self.userTab)
continue // I add this line too
}
}
})
print("userTab: \(self.userTab)") // Now the array isn't empty
}
In the For Loop I can access the variable "pseudo" and print all users nicknames in the console, but outside the For Loop I can't put the pseudo variable in the array userTab. How can I do that? I really need to fill my userTab with the nickname of users and show all in a TableView then.
I already try to declare the "pseudo" variable outside the viewDidLoad() method but same issue, the array is always empty after the For Loop. What I did wrong?
See my edit, now all the users nickname are in the userTab array and I can search for them in the search bar and display the result in the tableView.
observeSingleEvent is async, it is not outside loop but in different time,
try to set breakpoints to see where it stops first
you set self.userTab = [pseudo] so now just reload tableview

Setting collection view methods only after Firebase call is complete

I'm attempting to set the numberOfItemsInSection method of my collection view with the count of a certain dictionary. The dictionary is set from a Firebase call (code below if that part matters). I was under the impression that Firebase calls were asynchronous anyway, and wouldn't need to be combined with a dispatch queue, closure, or separate completion handler.
However, when I try to set numberOfItemsInSection to return avatarDictionary.count, it's empty, and indeed printing that count shows 0. The dictionary in question does get set with its values (printing confirms that), but it needs to loop through all the users I'm fetching data for before it has all its values. I think when numberOfItemsInSection checks its return, the dictionary is still at 0.
Is that what's happening? If so, what's the best way to make sure the dictionary is fully set with all its values before setting the collection view?
Code:
func getParticipantInfo() {
let databaseRef = FIRDatabase.database().reference()
// Query Firebase users with those UIDs & grab their gender, profilePicture, and name
databaseRef.child("groups").child(currentRoomID).child("participants").observeSingleEvent(of: .value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject] {
for each in snapDict {
let uid = each.key
let avatar = each.value["profilePicture"] as! String
let gender = each.value["gender"] as! String
let handle = each.value["handle"] as! String
let name = each.value["name"] as! String
// Set those to the dictionaries [UID : value]
self.avatarDictionary.setValue(avatar, forKey: uid)
self.nameDictionary.setValue(name, forKey: uid)
self.genderDictionary.setValue(gender, forKey: uid)
self.handleDictionary.setValue(handle, forKey: uid)
print("\n\navatarDictionary:\n \(self.avatarDictionary)")
print("\nhandleDictionary:\n \(self.handleDictionary)")
print("\ngenderDictionary:\n \(self.genderDictionary)")
print("\nnameDictionary:\n \(self.nameDictionary)")
}
}
}, withCancel: {(Err) in
print(Err.localizedDescription)
})
}
Use this after setting the array
collectionView.reloadData()
(Right after the for loop)

How to add one post at a time from Firebase?

I followed a tutorial to get a timeline going. In the tutorial he uses .Value when retrieving data from Firebase. I later learned that it isn't very efficient because it re-downloads everything whenever a post is added. This causes all user's timelines to flash and auto-scroll.
I'm trying to convert this to use .childAdded but can't figure out how to get it to work. This is the current code:
DataService.ds.REF_POSTS.child("\(self.loggedInUser!.uid)").observeEventType(.Value, withBlock: { postDictionary in
if postDictionary.exists() {
if let snapshots = postDictionary.children.allObjects as? [FIRDataSnapshot] {
self.posts = []
for snap in snapshots {
if let postDict = snap.value as? NSDictionary {
for(name, value) in postDict {
let interval = postDict.objectForKey("timePosted") as! Double
let formattedDate = NSDate(timeIntervalSince1970: interval)
let timeAgo = self.getDate(formattedDate)
if name as! String == "postedBy" {
DataService.ds.REF_USERS.child(value as! String).observeSingleEventOfType(.Value, withBlock: { (userDictionary) in
let userDict = userDictionary.value as! NSDictionary
let username = userDict.objectForKey("username")!
let profileThumbUrl = userDict.objectForKey("profileThumbUrl")!
let key = snap.key
let post = Post(postKey: key, dictionary: postDict, username: username as! String, profileThumbUrl: profileThumbUrl as! String, timeAgo: timeAgo)
self.posts.append(post)
self.collectionView?.reloadData()
})
}
}
}
}
}
}
})
The 2nd Firebase call is just grabbing user info for the user thumbnail and username. When I change .Value at the top to .ChildAdded, nothing shows up on the timeline. I have tried messing around with filetypes etc. and can't get it to work.
Update
Okay so after reading another Firebase question on here it looks like .ChildAdded gets just the child added. I was thinking it got the same data, just that the whole thing would run when a child was added.
So it looks like I'm going to have to change the loops here and run a loop adding each child added one by one. Going to redo the whole thing and will post the difference when I finish it if I can get it working.

Resources