iOS writing to Firebase leads to crash - swift

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.

Related

Displaying all posts using Firebase in Swift?

My goal is so that when users open the app they will see every post from every user of the app. My data tree is as follows:
- posts
- UID
- postKey
--attributes
- users
- UID
- attributes
Here's an example:
-posts
-74anqEXU8kQHVr7IKoO3N9NNqDh1
-MLzvs5VvXB_z7fhbh2p
created_at: 1605242945.969368
image_height: 414.6865671641791
image_url: "https://firebasestorage...."
-MLzvun01fNXNRbn7TPv
-MM7zGyZ7GbenhlisJUZ
-VGxqdzc2CkWWn39pa8xUofEWgNm2
-pNvGg84JR0TbXvS4XlX8KbfBasz2
-users
-74anqEXU8kQHVr7IKoO3N9NNqDh1
-VGxqdzc2CkWWn39pa8xUofEWgNm2
I know I have to make a snapshot of all the posts, and then display it in viewDidLoad() of the main controller (SearchViewController). I can't figure out how to display all posts.
This code works for displaying the Current User's posts, stored in a file called UserService.swift:
static func posts(for user: User, completion: #escaping ([Post]) -> Void) {
let ref = Database.database().reference().child("posts").child(user.uid)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let snapshot = snapshot.children.allObjects as? [DataSnapshot] else {
return completion([])
}
let dispatchGroup = DispatchGroup()
let posts: [Post] = snapshot.reversed().compactMap {
guard let post = Post(snapshot: $0) else { return nil }
dispatchGroup.enter()
SaveService.isPostSaved(post) { (isSaved) in
post.isSaved = isSaved
dispatchGroup.leave()
}
return post
}
dispatchGroup.notify(queue: .main, execute: {
completion(posts)
})
})
}
And then in ViewDidLoad() of SearchViewController:
UserService.posts(for: User.current) { (posts) in
self.posts = posts
self.tableView.reloadData()
}
But how do I do this for all posts. When I try the following, it won't let me specify a random user:
let ref = Database.database().reference().child("posts").child(User)
Any idea what I can put in the ".child(User" box to make this work? Or how to create a for-loop to properly iterate through?
*EDIT
Here is what worked:
static func allPosts(completion: #escaping ([Post]) -> Void) {
let ref = Database.database().reference().child("posts")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
var postsArray = [Post]()
for userSnapshot in snapshot.children {
guard let snapshot = (userSnapshot as AnyObject).children.allObjects as? [DataSnapshot] else {
return completion([])
}
let dispatchGroup = DispatchGroup()
let posts: [Post] = snapshot.reversed().compactMap {
guard let post = Post(snapshot: $0) else { return nil }
postsArray
postsArray.append(post)
dispatchGroup.enter()
SaveService.isPostSaved(post) { (isSaved) in
post.isSaved = isSaved
dispatchGroup.leave()
}
return post
}
dispatchGroup.notify(queue: .main, execute: {
completion(postsArray)
})
}
})
}
I may be overlooking something but it appears the goal is to
when users open the app they will see every post from every user
If that's the case, sometimes simpler is better; load all of the posts, iterate over them creating your Post object for each then reload the tableview (?)
let postsRef = self.ref.child("posts") //self.ref points to my firebase
postsRef.observeSingleEvent(of: .value, with: { snapshot in
let allPosts = snapshot.children.allObjects as! [DataSnapshot]
for postSnap in allPosts {
let aPost = Post(initWithSnapshot: postSnap)
self.postsArrray.append(post) //
}
self.postTableView.reloadData()
}
This assumes your Post class has an convenience init to populate its properties from the snapshot. If so, set isSaved to true within that since the convenience init is being called with saved data.
If you're calling this when the app starts or view loads, there's no need for callbacks and dispatchQueues.
You can observe one level higher in the JSON, and then add an extra loop in the callback:
let ref = Database.database().reference().child("posts")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
for userSnapshot in snapshot.children {
guard let snapshot = userSnapshot.children.allObjects as? [DataSnapshot] else {
return completion([])
}
let dispatchGroup = DispatchGroup()
let posts: [Post] = snapshot.reversed().compactMap {
guard let post = Post(snapshot: $0) else { return nil }
dispatchGroup.enter()
SaveService.isPostSaved(post) { (isSaved) in
post.isSaved = isSaved
dispatchGroup.leave()
}
return post
}
}
dispatchGroup.notify(queue: .main, execute: {
completion(posts)
})
})

Working with asynchronous data with Swift closures and Firebase

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.

Chaining promises in Swift to initialize a custom object

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]

Firebase database doesn't stop updating

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)
}
}

Dispatch Queue Async Call

I am firing off a network request inside a for loop that is within another network request. I'm using Core Data but I am fairly certain this is not a Core Data issue, and is an async issue.
The 2 print statements inside the the Firebase request print the data properly, but without the DispatchQueue the array returns as empty (before the network request completes).
Here's a picture of the crash:
Here is the code itself:
var userReps = [UserRepresentation]()
// Fetch all Friends -> update Core Data accordingly
func fetchFriendsFromServer(completion: #escaping (Error?) -> Void = { _ in}){
let backgroundContext = CoreDataStack.shared.container.newBackgroundContext()
// 1. Fetch all friends from Firebase
FirebaseDatabase.UserDatabaseReference.child(CoreUserController.shared.userPhoneNumber).child(UserKeys.UserFriends).child(UserKeys.UserAcceptedFriends).observe(.value) { (data) in
if let dictionary = data.value as? [String: String] {
var userReps = [UserRepresentation]()
let group = DispatchGroup()
group.enter()
for friend in dictionary {
let friendName = friend.value
let friendId = friend.key
FirebaseDatabase.UserDatabaseReference.child(friendId).observe(.value, with: { (data) in
if let dictionary = data.value as? [String: Any] {
guard let gender = dictionary[UserKeys.UserGender] as? String else {return}
guard let bio = dictionary[UserKeys.UserBio] as? String else {return}
guard let status = dictionary[UserKeys.UserStatus] as? String else {return}
guard let avatarUrl = dictionary[UserKeys.UserAvatarUrlKey] as? String else {return}
let friendRepresentation = UserRepresentation(avatarUrl: avatarUrl, name: friendName, number: friendId, gender: gender, status: status, bio: bio)
userReps.append(friendRepresentation)
print("HERE, friends fetched: ", friendRepresentation)
print("HERE, reps fetched: ", userReps)
group.leave()
}
})
}
group.notify(queue: .main) {
// 2. Update Core Data value with Firebase values
self.updateFriends(with: userReps, in: backgroundContext)
// 3. Save Core Data background context
do {
try CoreDataStack.shared.save(context: backgroundContext)
} catch let error {
print("HERE. Error saving changes to core data: \(error.localizedDescription)")
}
}
}
}
}
Any help would go a long way
Since
let group = DispatchGroup()
is a local variable and you use observe here
FirebaseDatabase.UserDatabaseReference.child(friendId).observe(.value, with: { (data) in
it will re-call it after function deallocation either make it an instance variable or make this single observe
FirebaseDatabase.UserDatabaseReference.child(friendId).observeSingleEvent(of:.value) { (data) in
Also make enter inside the for loop
for friend in dictionary {
group.enter()