I was trying to update my Firebase database and I ran into this problem. Take a look at the following code snippet and the screenshot:
func saveRetrieveStoryID(completion: #escaping (Bool) -> Void) {
let userID = Auth.auth().currentUser?.uid
//Create a reference to the database
let DBRef = Database.database().reference()
let storyIDRef = DBRef.child("Story IDs").child(userID!)
storyIDRef.observe(.value) { (snapshot) in
for childOne in snapshot.children {
print(childOne)
if let childOneSnapshot = childOne as? DataSnapshot {
storyIDKeyList.append(Int(childOneSnapshot.key)!)
print(childOneSnapshot.key)
completion(true)
}
}
print(storyIDKeyList)
}
}
What the code does is that it retrieves the key (-1) from the database and stores it inside a list (storyIDKeyList). Now take a look at the following code snippet:
saveRetrieveStoryID { (saved) in
if saved {
// Store the story ID in the user's story ID dict
let storyIDRef = DBRef.child("Story IDs").child(userID!)
let newStoryIDKey = storyIDKeyList.last! + 1
storyIDs[String(newStoryIDKey)] = storyRef.key
storyIDRef.updateChildValues(storyIDs, withCompletionBlock: { (error, ref) in
if let error = error?.localizedDescription {
print("Failed to update databse with error: ", error)
}
})
}
}
This piece of code, takes the last item from the storyIDKeyList and adds 1 to it. Then this will be added to the storyIDs dictionary storyIDs[String(newStoryIDKey)] = storyRef.key and the database will be update with the new key and value. But the problem is that, the database keeps on updating and it doesn't stop until I stop running the code. Here is a picture of the resulting database:
Notice that all the values are the same. This following screenshot should be the expected outcome:
I just want to add one key/value to the database each time I run the code; I kind of know why this is happening but I'm finding it difficult to solve this problem.
After a lot of tries, I managed to find a solution to this problem.
Edit: I found a better solution, thanks to this answer: Android Firebase Database keeps updating value. Using observeSingleEvent() retrieves the data only once.
Here is the code (Better answer IMO):
func saveRetrieveStoryID(completion: #escaping (Bool) -> Void) {
let userID = Auth.auth().currentUser?.uid
let storyIDRef = DBRef.child("Story IDs").child(userID!)
storyIDRef.observeSingleEvent(of: .value) { (snapshot) in
for childOne in snapshot.children {
if let childOneSnapshot = childOne as? DataSnapshot {
storyIDKeyList.append(Int(childOneSnapshot.key)!)
}
}
completion(true)
}
}
Old answer (Works too):
func saveRetrieveStoryID(completion: #escaping (Bool) -> Void) {
let userID = Auth.auth().currentUser?.uid
let storyIDRef = DBRef.child("Story IDs").child(userID!)
storyIDRef.observe(.value) { (snapshot) in
for childOne in snapshot.children {
if let childOneSnapshot = childOne as? DataSnapshot {
storyIDKeyList.append(Int(childOneSnapshot.key)!)
}
}
storyIDRef.removeAllObservers()
completion(true)
}
}
Related
I load some data to show them on a tableView. I try to remove the observer for this. Right now the observer shows duplicate content. It shows the a post 10-15 times.
This is how I observe:
func loadData(){
let placeIdFromSearch = ViewController.placeidUebertragen
ref = Database.database().reference().child("placeID/\(placeIdFromSearch)/\(ViewComments.subComment)/subcomment/")
ref.observe(.childAdded) { (snapshot) in
print("something changed")
guard let dic = snapshot.value as? [String: Any] else { return }
let newPost = importSubPosts(dictionary: dic, key: snapshot.key)
guard let userUid = newPost.userID else { return }
self.fetchUser(uid: userUid, completed: {
self.table.insert(newPost, at: 0)
self.tableView.reloadData()
})
}
}
func fetchUser(uid: String, completed: #escaping () -> Void) {
ref = Database.database().reference().child("user").child(uid)
ref.observe(.value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
self.users.insert(newUser, at: 0)
completed()
}
}
This should remove the observer:
ref = Database.database().reference().child("placeID/\(placeIdFromSearch)/\(ViewComments.subComment)/subcomment/")
ref.removeAllObservers()
But nothing happens.
PS: If it helps if I deactivate this part the error goes away. But then I don't have user profile picture. Is It because I use this func already on a different tableView?
ref.observe(.value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
self.users.insert(newUser, at: 0)
The Firebase documentation provides a number of examples of how to read and write data
In this case it appears there are a couple of issues;
One is how to implement listeners:
.childAdded - This event is triggered once for each existing child and
then again every time a new child is added to the specified path
ChildAdded is great for initially loading a list of data e.g. several child nodes and watching for additions to that list thereafter. If you want to load the data and then stop listening, you can remove the listener from inside the .childAdded closure (there are other options as well)
If you're interested in loading just a single node, 'subcomment' see the load single node section below
If, however subcomment should be plural 'subcomments' because there are several, you can user this pattern to load them without leaving an observer
let usersRef = my_firebase.child("users")
usersRef.observeSingleEvent(by: .value, with: { snapshot in
//all users are returned to the app in the snapshot
// .value means 'give me everything in the specified node'
// so then we can iterate over the snapshot to get each users data
}
Load single node -
The second issue which ties to the first issue within the 'fetchUser' function - it appears you want to read in a single user from Firebase, one time.
While you can use .observe to do that, it actually reads in ALL data and listens for all events ongoing which doesn't appear like you want to do.
A better solution is to read the user one time with observeSingleEvent aka getData that reads it in and doesn't leave a listener - therefore eliminating the need to remove observers when the read is done.
There's a perfect example in the documentation that you could almost copy and paste the code - it reads a single user, and doesn't leave an observer. It could be used for reading the subcomment as well without leaving a listener attached. See the code at Read Data Once
ref.child("users/\(uid)/username").getData(completion: { error, snapshot in
guard error == nil else {
print(error!.localizedDescription)
return;
}
let userName = snapshot.value as? String ?? "Unknown";
});
I need to call a specific Firebase reference and get back data. This operation will take place inside multiple VCs so I want to have a class where I will have various functions calling Firebase. For example, if I want to get all articles I will call my FirebaseHelpers class, and use the method/closure fetchArticles(). This way, if I want to refactor something I will only do it in FirebaseHelpers class, and not go through all VCs.
FirebaseHelpers
import UIKit
import Firebase
class FirebaseHelpers {
func fetchArticles(completion: #escaping ([Article]?, Error?) -> Void) {
var articles = [Article]()
let articlesQuery = Database.database().reference().child("articles").queryOrdered(byChild: "createdAt")
articlesQuery.observe(.value, with: { (snapshot) in
guard let articlesDictionaries = snapshot.value as? [String : Any] else { return }
articlesDictionaries.forEach({ (key, value) in
guard let articleDictionary = value as? [String: Any] else { return }
// build articles array
let article = Article(dictionary: articleDictionary)
print("this is article within closure \(article)")
articles.append(article)
})
})
completion(articles, nil)
}
}
In any viewController
let firebaseHelpers = FirebaseHelpers()
var articles = [Article]() {
didSet {
self.collectionView.reloadData()
}
}
// this is inside viewDidLoad()
firebaseHelpers.fetchArticles { (articles, error) in
guard let articles = articles else { return }
print("articles \(articles)")
self.articles = articles
}
The problem is that I don't get any results back. In my VC the print("articles (articles)") will return an empty array. But in my FirebaseHelpers fetchArticles() the print("this is article within closure (article)") will print the article(s) just fine.
Any idea why this is happening?
Thanks in advance.
You can move completion inside your asynchronous function:
class FirebaseHelpers {
func fetchArticles(completion: #escaping ([Article]?, Error?) -> Void) {
var articles = [Article]()
let articlesQuery = Database.database().reference().child("articles").queryOrdered(byChild: "createdAt")
articlesQuery.observe(.value, with: { (snapshot) in
guard let articlesDictionaries = snapshot.value as? [String : Any] else { return }
articlesDictionaries.forEach({ (key, value) in
guard let articleDictionary = value as? [String: Any] else { return }
// build articles array
let article = Article(dictionary: articleDictionary)
print("this is article within closure \(article)")
articles.append(article)
})
completion(articles, nil) // <- move here
})
// completion(articles, nil) // <- remove
}
}
Otherwise completion will be called before your asynchronous function.
I implemented PromiseKit in Swift to avoid callback hell with completion blocks. I need to know the best way to chain promises together to init custom objects that have other associated objects. For example a Comment object that has a User object attached to it.
First I fetch the comments from the DB, which all have a uid property in the DB structure. I ultimately want to end up with an array of comments, where each one has the correct user attached to it, so I can load both the comment and user data. This all seemed much easier with completion blocks, but I'm a total Promise noob so idk.
Here is the code in the controller that handles fetch
CommentsService.shared.fetchComments(withPostKey: postKey)
.then { comments -> Promise<[User]> in
let uids = comments.map({ $0.uid })
return UserService.shared.fetchUsers(withUids: uids)
}.done({ users in
// how to init Comment object with users now?
})
.catch { error in
print("DEBUG: Failed with error \(error)")
}
Here is comment fetch function:
func fetchComments(withPostKey postKey: String) -> Promise<[Comment]> {
return Promise { resolver in
REF_COMMENTS.child(postKey).observeSingleEvent(of: .value) { snapshot in
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
let data = Array(dictionary.values)
do {
let comments = try FirebaseDecoder().decode([Comment].self, from: data)
resolver.fulfill(comments)
} catch let error {
resolver.reject(error)
}
}
}
}
Here is fetch users function
func fetchUsers(withUids uids: [String]) -> Promise<[User]> {
var users = [User]()
return Promise { resolver in
uids.forEach { uid in
self.fetchUser(withUid: uid).done { user in
users.append(user)
guard users.count == uids.count else { return }
resolver.fulfill(users)
}.catch { error in
resolver.reject(error)
}
}
}
}
Here is comment object:
struct Comment: Decodable {
let uid: String
let commentText: String
let creationDate: Date
var user: User?
}
This is how simple it is with completion blocks, starting to think Promises aren't worth it?
func fetchComments(withPostKey postKey: String, completion: #escaping([Comment]) -> Void) {
var comments = [Comment]()
REF_COMMENTS.child(postKey).observe(.childAdded) { (snapshot) in
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
guard let uid = dictionary["uid"] as? String else { return }
UserService.shared.fetchUser(withUid: uid, completion: { (user) in
let comment = Comment(user: user, dictionary: dictionary)
comments.append(comment)
completion(comments)
})
}
}
Ok I think I see what you are trying to do. The issue is that you need to capture the comments along with the users so you can return then together and later combine them. It should look something like this:
CommentsService.shared.fetchComments(withPostKey: postKey)
.then { comments -> Promise<[Comment], [User]> in
let uids = comments.map({ $0.uid })
return UserService.shared.fetchUsers(withUids: uids)
.then { users in
return Promise<[Comment], [User]>(comments, users)
}
}.done({ combined in
let (comments, users) = combined
//Do combiney stuff here
})
.catch { error in
print("DEBUG: Failed with error \(error)")
}
The transforms are [Comment] -> [User] -> ([Comment], [User]) -> [Comments with users attached]
Im retrieving data from a firebase backend which is below. My JSON structure has 2 child notes from my understanding With the code in my view did load, i can access the users node and i can print the "email" & "provider" -
However, my main goal is to actually access the 'planits' node and get the "images" & "planit" details. I am just pretty stuck on a way to implement that. I would appreciate any and all of the help provided. Thank you!
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
refHandle = ref.observe(DataEventType.value, with: { (snapshot) in
let dataDict = snapshot.value as! [String: AnyObject]
print(dataDict)
})
let userID = Auth.auth().currentUser?.uid
ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
//Get user value
if snapshot.exists() == true {
for child in snapshot.children {
let value = snapshot.value as? NSDictionary
let EmailString = value!["email"] as! String
self.userEmail.append(EmailString)
print(self.userEmail)
// BELOW IS MY FAILED ATTEMPT AT ACCESSING THE 'planits' node - Nothing Prints
let image = value!["images"] as! String
print(image)
}
}
}) { (error) in
print(error.localizedDescription)
}
I wonder if my code is thread safe, in tableView(_ tableView:, leadingSwipeActionsConfigurationForRowAt indexPath:) I create an action that accepts a friend request. The method is invoked from a blok of UIContextualAction(style: .normal, title: nil) { (action, view, handler) in }
The actual Firebase call is like this:
class func acceptInvite(uid: String, completion: #escaping (Bool)->Void) {
guard let user = currentUser else { completion(false); return }
usersRef.child(user.uid).child("invites").queryEqual(toValue: uid).ref.removeValue()
usersRef.child(user.uid).child("friends").childByAutoId().setValue(uid)
usersRef.child(uid).child("friends").childByAutoId().setValue(user.uid)
completion(true)
}
image from debug navigator
It would be great if someone had an explanation.
edit: I think the problem is in my async loop to get the userdata
class func get(type: String, completion: #escaping ([Friend])->Void) {
let usersRef = Database.database().reference().child("users")
guard let user = currentUser else { completion([]); return }
usersRef.child(user.uid).child(type).observe(.value){ (snapshot) in
guard let invitesKeyValues = snapshot.value as? [String: String] else { completion([]); return }
var optionalFriendsDictArray: [[String: Any]?] = []
let dispatchGroup = DispatchGroup()
for (_, inviteUID) in invitesKeyValues {
dispatchGroup.enter()
usersRef.child(inviteUID).observe(.value, with: { (snapshot) in
let friend = snapshot.value as? [String: Any]
optionalFriendsDictArray.append(friend)
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: DispatchQueue.global(), execute: {
let friends = optionalFriendsDictArray.flatMap({ (optional) -> Friend? in
Friend.init(userDictionary: optional)
})
completion(friends)
})
}
}
This problem really gets me thinking about Firebase usage. I could add more information about the user at the friends key of a user so you don't have to query all the user to populate a small list with a name and a photo.
But what about viewing your friends posts on your timeline, your definitely not going to copy every friends' post into the users object. ???
I solved this problem by fetching the data with an observe single event and using the childadded and childremoved observers for mutations.