(Swift + Firebase) Convert observeSingleEvent to observe .childAdded - ios

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.

Related

Firebase query results are mistaken

This is my database design.
foodie-ab2b4{
Foods{
0{
FoodName: "Baked Beans In Tomato Sauce"
FoodRecipe:
FoodUri:
Image:
}
1{
FoodName: "Another bean | Bubbling Bacon Butter Beans recipes"
FoodRecipe:
FoodUri:
Image:
}
}
}
I'm working on an ios project and this is how my firebase json structured.
let ref = Database.database().reference()
func searchFoodByName(FoodName: String){
let foodsRef = ref.child("Foods")
let input = FoodName
let query = foodsRef.child(key).queryOrdered(byChild: "FoodName").queryEnding(atValue: input)
query.observeSingleEvent(of: .value, with: { snapshot in
print(snapshot)
for child in snapshot.children {
let snap = child as! DataSnapshot
let dict = snap.value as! [String: Any]
let fName = dict["FoodName"] as! String
let fIngredients = dict["Ingredients"] as! [String]
print(fName)
print(fIngredients)
let key = snapshot.key
print(key)
}
})
}
I'm trying to filter my food objects by their names. I have 10 objects in my database. In most cases this search return true objects. But there is one example i can not solve.
When i query the "tomato" word i need to get 1 object. But there is two. Although there is no tomato word in my second object.
The two object is in my json file.
If it is necessary i can upload my whole JSON file to here.
You seem to assume that Firebase can filter based on strings that contain a value, but it can't. See Firebase query - Find item with child that contains string (and the many links from there).
What Firebase can do is search for string values that start with a certain value. You do that by using a combination of queryStarting(atValue:) and queryEnding(atValue:):
let query = foodsRef
.queryOrdered(byChild: "FoodName")
.queryStarting(atValue: input)
.queryEnding(atValue: input+"\\uf8ff")
If you use this query, and input is ``Baked`, it will only match the first food from your JSON.

Retrieving data from Firebase and using it in another Firebase database reference

My data structure is something like the following:
restaurant_owners
|
|owner_id (a unique ID)
|
|restaurant_name
|email
restaurant_menus
|
|restaurant_name
|
|dish_type (drinks, appetizer, etc...)
|
|dish_id (a unique ID)
|
|name
|
|price
The idea of the app is basically to allow "restaurant_owners" to login and manage the menu of their respective restaurant. However I am having problems with the following code: (note that the fetchDish function is called in viewDidLoad)
func fetchDish() {
var restaurantName: String?
let uid = FIRAuth.auth()?.currentUser?.uid
//first time referencing database
FIRDatabase.database().reference().child("owners").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
DispatchQueue.main.async{
restaurantName = dictionary["name"] as? String
print(restaurantName!)
}
}
})
//second time referencing database
FIRDatabase.database().reference().child("restaurants").child(restaurantName!).child("appetizer").observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let dish = Dish()
dish.setValuesForKeys(dictionary)
self.dishes.append(dish)
DispatchQueue.main.async {
self.tableview.reloadData()
}
}
}, withCancel: nil)
}
What I am trying to do is to retrieve the the name of the restaurant for the current logged in user and store it in the variable "restaurantName". Then when I am referencing the database for the second time I can use this variable inside of .child (e.g.: .child(restaurantName)).
However, when I run this, I get an error saying that the restaurantName (in the database reference) is of value nil. I tried putting in some breakpoints and it seems like the first line of the second database reference is operated before whatever is "within" the first database reference, so basically restaurantName is called before any value is stored in it.
Why is this occurring? How do I work around this problem? Also, what are the best practices to achieve this if I'm doing it completely wrong?
NoSQL is very new to me and I have completely no idea how I should design my data structure. Thanks for the help in advance and please let me know if you need any other information.
UPDATE:
The problem was solved by changing my data structure to what Jay has suggested. The following code is what worked for me: (modified Jay's code a bit)
func fetchOwner() {
let uid = FIRAuth.auth()?.currentUser?.uid
let ownersRef = FIRDatabase.database().reference().child("owners")
ownersRef.child(uid!).observeSingleEvent(of: .value, with: { snapshot in
if let dict = snapshot.value as? [String: AnyObject] {
let restaurantID = dict["restaurantID"] as! String
self.fetchRestaurant(restaurantID: restaurantID)
}
}, withCancel: nil)
}
func fetchRestaurant(restaurantID: String) {
let restaurantsRef = FIRDatabase.database().reference().child("restaurants")
restaurantsRef.child(restaurantID).child("menu").observe(.childAdded, with: { snapshot in
if let dictionary = snapshot.value as? [String: AnyObject] {
let dish = Dish()
dish.setValuesForKeys(dictionary)
self.dishes.append(dish)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}, withCancel: nil)
}
A couple of things:
Firebase is Asynchronous and you have to account for that in your code. As it is in the post, the second Firebase function may execute before the first Firebase function has successfully returned data i.e. restaurantName may be nil when the second call happens.
You should nest your calls (in this use case) to ensure data is valid before working with it. Like this.. and keep reading
let ownersRef = rootRef.child("owners")
let restaurantRef = rootRef.child("restaurants")
func viewDidLoad() {
fetchOwner("owner uid")
}
func fetchOwner(ownerUid: String) {
var restaurantName: String?
let uid = FIRAuth.auth()?.currentUser?.uid
ownserRef.child(ownerUid).observeSingleEvent(of: .value, with: { snapshot in
if let dict = snapshot.value as? [String: AnyObject] {
restaurantId = dict["restaurant_id"] as? String
fetchRestaurant(restaurantId)
}
}
})
}
func fetchRestaurant(restaurantId: String) {
restaurantRef.child(restaurantId).observeSingleEvent(of: .value, with: { snapshot in
if let dict = snapshot.value as? [String: AnyObject] {
let restaurantName = dict["name"] as! String
let menuDict = dict["menu"] as! [String:Any]
self.dataSourceArray.append(menuDict)
menuTableView.reloadData()
}
}
}
Most importantly, it's almost always best practice to disassociate your key names from the data it contains. In this case, you're using the restaurant name as the key. What if the restaurant name changes or is updated? You can't change a key! The only option is to delete it and re-write it.... and... every node in the database that refers to it.
A better options it to leverage childByAutoId and let Firebase name the nodes for you and keep a child that has the relevant data.
restaurants
-Yii9sjs9s9k9ksd
name: "Bobs Big Burger Barn"
owner: -Y88jsjjdooijisad
menu:
-y8u8jh8jajsd
name: "Belly Buster Burger"
type: "burger"
price: "$1M"
-j8u89joskoko
name: "Black and Blue Burger"
type: "burger"
price: "$9.95"
As you can see, I leveraged childByAutoId to create the key for this restaurant, as well as the items on the menu. I also referenced the owner's uid in the owner node.
In this case, If the Belly Buster Burger changes to the Waist Slimming Burger, we can make one change and it's done and anything that references it is also updated. Same thing with the owner, if the owner changes, then just change the owner uid.
If the restaurant name changes to Tony's Taco Tavern, just change the child node and it's done.
Hope that helps!
edit: Answer to a comment:
To get the string (i.e. the 'key' of a key:value pair) immediately created by .childByAutoId()
let testRef = ref.child("test").childByAutoId()
let key = testRef.key
print(key)

How to ensure that the data is not retrieved and appended as a whole each time a new entry is added?

func generateDataForRecents() {
if URLArrayStringThisSeason.count == 0 {
self.activityIndicator2.isHidden = false
self.activityIndicator2.startAnimating()
}
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("palettes").queryLimited(toFirst: 100).observe(.value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject]{
for each in snapDict as [String:AnyObject]{
let URL = each.value["URL"] as! String
self.URLArrayStringRecents.append(URL)
//print(self.URLArrayString.count)
//print(snapshot)
//let pictureTitle = each.value["title"] as! String
print(self.URLArrayStringRecents.count)
}
}
self.whatsNewCollectionView?.reloadData() //Reloads data after the number and all the URLs are fetched
self.activityIndicator2.stopAnimating()
self.activityIndicator2.isHidden = true
})
}
The following code does a retrieval of data each time the function is called, or when a new data is added.
This is extremely useful when the app is first started up or closed and then restarted. However, when the app is running, whenever a new entry is added, the code seemed to run again and thus appending twice the amount of new data.
For example, when there are already 15 entries identified and then suddenly a new entry is added, the array of the URL would contain 15+16 thus amounting to a total of 31.
How do I make it such that the new data is added to the array instead of adding the entire snapshot in?
You do that by listening for .childAdded events, instead of listening for .value:
var query = databaseRef.child("palettes").queryLimited(toFirst: 100)
query.observe(.childAdded, with: { (snapshot) in
let URL = snapshot.childSnapshot(forPath/: "URL").value as! String
self.URLArrayStringRecents.append(URL)
}
Since you have a limit-query, adding a 101st item means that one item will be removed from the view. So you'll want to handle .childRemoved too:
query.observe(.childRemoved, with: { (snapshot) in
// TODO: remove the item from snapshot.key from the araay
})
I recommend that you spend some time in the relevant documentation on handling child events before continuing.
Please check below method. I have use this method not getting any duplicate entry.
func getallNotes()
{
let firebaseNotesString: String = Firebase_notes.URL
let firebaseNotes = FIRDatabase.database().referenceFromURL(firebaseNotesString).child(email)
firebaseNotes.observeEventType(.Value, withBlock: { snapshot in
if snapshot.childSnapshotForPath("Category").hasChildren()
{
let child = snapshot.children
self.arrNotes = NSMutableArray()
self.arrDictKeys = NSMutableArray()
for itemsz in child
{
let childz = itemsz as! FIRDataSnapshot
let AcqChildKey : String = childz.key
if AcqChildKey == AcqIdGlobal
{
if (childz.hasChildren() == true)
{
let dictChild = childz.value as! NSMutableDictionary
self.arrDictKeys = NSMutableArray(array: dictChild.allKeys)
for i in 0..<self.arrDictKeys.count
{
let _key = self.arrDictKeys.objectAtIndex(i).description()
print(_key)
let dictData : NSMutableDictionary = NSMutableDictionary(dictionary: (dictChild.valueForKey(_key)?.mutableCopy())! as! [NSObject : AnyObject])
dictData.setObject(_key, forKey: "notesId")
self.arrNotes.addObject(dictData)
}
}
}
}
self.tableviewNote.reloadData()
}
})
}
As for the query for removed child,
query.observe(.childRemoved, with: { (snapshot) in
print(snapshot)
let URL = snapshot.childSnapshot(forPath: "URL").value as! String
self.URLArrayStringThisSeason = self.URLArrayStringThisSeason.filter() {$0 != URL}
self.thisSeasonCollectionView.reloadData()
})
it will obtain the URL of the removed child and then update the array accordingly.

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.

How to retrieve objects from firebase by key value

I'm new to firebase and I have such structure of my firebase project
I want to get all objects, that "Interested" value is equal to "men"
I wrote such code, to get all object sorted by interes value:
let thisUserRef = URL_BASE.childByAppendingPath("profile")
thisUserRef.queryOrderedByChild("Interest")
.observeEventType(.Value, withBlock: { snapshot in
if let UserInterest = snapshot.value!["Interest"] as? String {
print (snapshot.key)
}
}
But I receive nil.
you need to loop through all the key-value profiles
if let allProfiles = snapshot.value as? [String:AnyObject] {
for (_,profile) in allProfiles {
print(profile);
let userInterest = profile["Interest"]
}
}
Here _ is the key that is in the format KYXA-random string and profile will be the element for that key.
Edit:
There is querying for child values as per the docs.
Try thisUserRef.queryOrderedByChild("Interest").equalTo("men") and then using the inner loop that i specified in the answer
This is a basic query in Firebase. (Updated for Swift 3, Firebase 4)
let profileRef = self.ref.child("profile")
profileRef.queryOrdered(byChild: "Interest").queryEqual(toValue: "men")
profileRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let dict = child as! [String: Any]
let name = dict["Name"] as! String
print(name)
}
})
The legacy documentation from Firebase really outlines how to work with queries: find it here
Legacy Firebase Queries
The new documentation is pretty thin.
Oh, just to point out the variable; thisUserNode should probably be profileRef as that's what you are actually query'ing.

Resources