How to use flatmap to store snapshot data into a variable - swift

Using the debugger, it shows that snapshot has 3 values but posts has 0 so I think I'm incorrectly using flatMap. Perhaps there is a better way to store the data into posts.
static func observePosts(for user: User = User.current, withCompletion completion: #escaping (DatabaseReference, [Post]) -> Void) -> DatabaseHandle {
let ref = Database.database().reference().child("posts").child(user.username)
return ref.observe(.value, with: { (snapshot) in
guard let snapshot = snapshot.children.allObjects as? [DataSnapshot] else {
return completion(ref, [])
}
let posts = snapshot.flatMap(Post.init)
completion(ref, posts)
})
}

I think snapshot is just a picture of the data available at that time. You cannot directly match the snapshot objects to Post type. Please do as follows
let posts = snapshot.flatMap({(post) -> Post in
let value = post.value as? Dictionary<String, String>
let id = value?["id"] ?? ""
let author = value?["author"] ?? ""
let title = value?["title"] ?? ""
let body = value?["body"] ?? ""
return Post(id: id, author: author, title: title, body: body)
})

Related

Firebase dataSnapshot sub-node

I am trying to snapshot the shopping lists of a user. The snapshot works fine, except for the products node.
Link to db:
This is my observer:
func observeLists(callBack: #escaping (_ list: [List]) -> Void){
let dbRef = database.child("list").child(ShareData.shared.userId)
dbRef.queryOrdered(byChild: "completed").observe(.value, with: { snapshot in
var newItems: [List] = []
for child in snapshot.children {
if let snapshot = child as? DataSnapshot {
let groceryItem = List(snapshot: snapshot)
newItems.append(groceryItem)
}
}
callBack(newItems)
})
}
And this is my init(snapshot) from struct List
init(snapshot: DataSnapshot) {
let id = snapshot.key
let value = snapshot.value as? NSDictionary
let products = value?["products"] as? [Product] ?? []
let completed = value?["completed"] as? Bool ?? false
let storeId = value?["storeId"] as? String ?? ""
let name = value?["name"] as? String ?? ""
self.init(id, products, completed, storeId, name)
}
The products node in your database is not an array of products ([Product]), but rather a dictionary/map ([String: Product]).

Get data from firestore and assign it to an array of dictionaries

I am trying to get data from firestore collection and assign it to an array of dictionaries. for this part of the code below... i get the error "Cast from 'QuerySnapshot?' to unrelated type '[[String : Any]]' always fails" and the console prints "is not working".
guard let snap = snapshot as? [[String:Any]] else {
print("is not working")
completion(.failure(DatabaseError.failedToFetch))
return
}
Here is the full code.
// fetches and returns all conversations for the user with passed in uid
public func getAllConversations(for uid: String, completion: #escaping(Result<[Conversation], Error>) -> Void) {
print("fetching all convos")
//NEW
let db = Firestore.firestore()
let CurrentUser = Auth.auth().currentUser?.uid
let ListRef = db.collection("users").document(CurrentUser!).collection("conversations")
// fetch the current users convo list
ListRef.getDocuments { snapshot, error in
if let err = error {
debugPrint("Error fetching documents: \(err)")
} else {
guard let snap = snapshot as? [[String:Any]] else {
print("is not working")
completion(.failure(DatabaseError.failedToFetch))
return
}
print("is working")
let conversations: [Conversation] = snap.compactMap({ dictionary in
guard let id = dictionary["id"] as? String,
let name = dictionary["name"] as? String,
let otherUserUID = dictionary["other_user-uid"] as? String,
let latestMessage = dictionary["latest-message"] as? [String:Any],
let date = latestMessage["date"] as? String,
let message = latestMessage["message"] as? String,
let isRead = latestMessage["is-read"] as? Bool else {
return nil
}
//save other user ID to a global var
self.test = otherUserUID
//assign data into an array of dictionaries
let latestConvoObject = LatestMessage(date: date, text: message, isRead: isRead)
return Conversation(id: id, name: name, otherUserUid: otherUserUID, latestMessage: latestConvoObject)
})
completion(.success(conversations))
}
}
}
There are a numbers of way to read that data, and the process can be simplified by conforming objects to the codable protocol but let me provide a straight forward example. I don't know what your Conversation object looks like so here's mine
class ConversationClass {
var from = ""
var to = ""
var msg = ""
var timestamp = 0
convenience init(withDoc: DocumentSnapshot) {
self.init()
self.from = withDoc.get("from") as? String ?? "no from"
self.to = withDoc.get("to") as? String ?? "no to"
self.msg = withDoc.get("msg") as? String ?? "no msg"
self.timestamp = withDoc.get("timestamp") as? Int ?? 0
}
}
and then here's the the code that reads in all the conversation documents from a Collection, stores each in a ConversationClass object, puts those in an array and returns it through an escaping completion handler
func getConversations(completion: #escaping( [ConversationClass] ) -> Void) {
let conversationCollection = self.db.collection("conversations")
conversationCollection.getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else { return }
var convoArray = [ConversationClass]()
for doc in docs {
let convo = ConversationClass(withDoc: doc)
convoArray.append(convo)
}
completion(convoArray)
})
}

How to instantiate a class with asynchronous Firebase methods in Swift?

I am instantiating a User class via a Firebase DataSnapshot. Upon calling the initializer init(snapshot: DataSnapshot), it should asynchronously retrieve values from two distinct database references, namely pictureRef and nameRef, via the getFirebasePictureURL and getFirebaseNameString methods' #escaping completion handlers (using Firebase's observeSingleEvent method). To avoid the 'self' captured by a closure before all members were initialized error, I had to initialize fullName and pictureURL with temporary values of "" and URL(string: "initial"). However, when instantiating the class via User(snapshot: DataSnapshot), these values are never actually updated with the retrieved Firebase values.
import Firebase
class User {
var uid: String
var fullName: String? = ""
var pictureURL: URL? = URL(string: "initial")
//DataSnapshot Initializer
init(snapshot: DataSnapshot) {
self.uid = snapshot.key
getFirebasePictureURL(userId: uid) { (url) in
self.getFirebaseNameString(userId: self.uid) { (fullName) in
self.fullName = fullName
self.profilePictureURL = url
}
}
func getFirebasePictureURL(userId: String, completion: #escaping (_ url: URL) -> Void) {
let currentUserId = userId
//Firebase database picture reference
let pictureRef = Database.database().reference(withPath: "pictureChildPath")
pictureRef.observeSingleEvent(of: .value, with: { snapshot in
//Picture url string
let pictureString = snapshot.value as! String
//Completion handler (escaping)
completion(URL(string: pictureString)!)
})
}
func getFirebaseNameString(userId: String, completion: #escaping (_ fullName: String) -> Void) {
let currentUserId = userId
//Firebase database name reference
let nameRef = Database.database().reference(withPath: "nameChildPath")
nameRef.observeSingleEvent(of: .value, with: { snapshot in
let fullName = snapshot.value as? String
//Completion handler (escaping)
completion(fullName!)
})
}
}
Is there a reason this is happening, and how would I fix this so it does initialize to the retrieved values instead of just remaining with the temporary values? Is it because init isn't asynchronous?
Edit: I am reading data from one node of the Firebase database and, using that data, creating a new node child. The method that initializes the User class will create this new node in the database as:
As you can see, the children are updated with the temporary values so it seems the program execution does not wait for the callback.
Any help would be much appreciated!
By the comments, it seems we could reduce the code considerably which will also make it more manageable
(SEE EDIT)
Start with a simpler User class. Note that it is initialized by passing the snapshot and then reading the child nodes and populating the class vars
class UserClass {
var uid = ""
var username = ""
var url = ""
init?(snapshot: DataSnapshot) {
self.uid = snapshot.key
self.username = snapshot.childSnapshot(forPath: "fullName").value as? String ?? "No Name"
self.url = snapshot.childSnapshot(forPath: "url").value as? String ?? "No Url"
}
}
then the code to read a user from Firebase and create a single user
func fetchUser(uidToFetch: String) {
let usersRef = self.ref.child("users")
let thisUserRef = usersRef.child(uidToFetch)
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exists() {
let user = UserClass(snapshot: snapshot)
//do something with user...
} else {
print("user not found")
}
})
}
I don't know how the user is being used but you could add a completion handler if you need to do something else with the user outside the Firebase closure
func fetchUser(uidToFetch: String completion: #escaping (UserClass?) -> Void) {
//create user
completion(user)
EDIT:
Based on additional info, I'll update the answer. Starting with restating the objective.
The OP has two nodes, a node that stores user information such as name and another separate node that stores urls for pictures. They want to get the name from the first node, the picture url from the second node and create a new third node that has both of those pieces of data, along with the uid. Here's a possible structure for pictures
pictureUrls
uid_0: "some_url/uid_0"
uid_1: "some_url/uid_1"
and then we'll use the same /users node from above.
Here's the code that reads the name from /users, the picture url from /pictureUrls combines them together and writes out a new node with an /author child that contains that data and the uid.
func createNode(uidToFetch: String) {
let usersRef = self.ref.child("users")
let thisUserRef = usersRef.child(uidToFetch)
let imageUrlRef = self.ref.child("pictureUrls")
let thisUsersImageRef = imageUrlRef.child(uidToFetch)
let allAuthorsRef = self.ref.child("allAuthors")
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
let userName = snapshot.childSnapshot(forPath: "name").value as? String ?? "No Name"
thisUsersImageRef.observeSingleEvent(of: .value, with: { imageSnap in
let imageUrl = imageSnap.value as? String ?? "No Image Url"
let dataToWrite = [
"full_name": userName,
"profile_picture": imageUrl,
"uid": uidToFetch
]
let thisAuthorRef = allAuthorsRef.childByAutoId()
let authorRef = thisAuthorRef.child("author")
authorRef.setValue(dataToWrite)
})
})
}
The output to firebase is this
allAuthors
-LooqJlo_Oc-voUHai3k //created with .childByAutoId
author
full_name: "Leroy"
profile_picture: "some_uid/uid_0_pic"
uid: "uid_0"
which exactly matches the output shown in the question.
I removed the error checking to shorten the answer so please add that back in and I also omitted the callback since it's unclear why one it needed.
This is very hacky.
You should add completionHandler in init method. So, when your asynchronous call completed you will get actual value of object.
init(snapshot: DataSnapshot, completionHandler: #escaping (User) -> Void) {
self.uid = snapshot.key
getFirebasePictureURL(userId: uid) { (url) in
self.getFirebaseNameString(userId: self.uid) { (fullName) in
self.fullName = fullName
self.profilePictureURL = url
completionHandler(self)
}
}
}
I hope this will help you.

Trouble using child snapshot inside for loop

I have a Firebase DB with "post/(randID)" structure, and Post class that inherits from an Item class. I already wrote a snapshot function that properly takes the value of all child nodes, but am now trying to only take a snapshot of post/ children that match elements of a name array I already have.
I'm properly getting values but not correctly appending temp values to my Item array at the breakpoint. Any help would be much appreciated
----------- CODE -----------
func getWavePosts() {
self.tempPosts = []
for name in self.tempNames {
var postRef = Database.database().reference().child("posts/\(name)")
postRef.observe(.value, with: {snapshot in
var test = snapshot.value as? [String:Any]
var author = test!["author"] as? [String:Any]
var uid = author!["uid"] as? String
var username = author!["username"] as? String
var photoURL = author!["photoURL"] as? String
var url = URL(string: photoURL!)
var imageURL = test!["imageURL"] as? String
var text = test!["text"] as? String
var timestamp = test!["timestamp"] as? Double
var userProfile = UserProfile(uid: uid!, username: username!, photoURL: url!)
var post = Post(id: name, author: userProfile, text: text!, timestamp: timestamp!, imageURL: imageURL!)
self.tempPosts.append(post)
//print(self.tempPosts)
//self.items = self.tempPosts
})
//self.items = self.tempPosts
}
print(self.tempPosts.count)
print(self.items.count)
}
First, your function should have completion with array of Post as parameter
func getWavePosts(_ completion: #escaping ([Post]) -> () )
...now let's meet with DispatchGroup.
First declare new DispatchGroup before foreach loop. Then before you observe postRef enter to dispatchGroup and after you append received Post to an array (define this array within function, don't use global variable) leave dispatchGroup. When every Post is added to an array, call completion in closure of dispatchGroup.notify(queue:)
func getWavePosts(_ completion: #escaping ([Post]) -> () ) {
var tempPosts = []
let dispatchGroup = DispatchGroup()
for name in self.tempNames {
dispatchGroup.enter()
var postRef = Database.database().reference().child("posts/\(name)")
postRef.observe(.value, with: { snapshot in
...
tempPosts.append(post)
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: .main) {
completion(tempPosts)
}
}
Then you have access to your received posts in closure of this method when you call it
getWavePosts { posts in
... // do whatever you want to
}

modifying a variable from inside a closure swift/firebase

In the function below, everything works except when I try to get the variable partnerName from point A to point B, i.e. moving the variable in an out of a closure. I am a novice coder so I am hoping that someone can help me discover how to solve this particular issue and point me to a place where I might be able to learn the basics of how to share variables between different parts of a function.
func getAllConversations(handler: #escaping (_ conversationsArray: [Conversation]) -> ()) {
var partner = [""]
var title: String = ""
var partnerName = ""
var conversationsArray = [Conversation]()
REF_CONVERSATION.observeSingleEvent(of: .value) { (conversationSnapshot) in
guard let conversationSnapshot = conversationSnapshot.children.allObjects as? [DataSnapshot] else { return}
for conversation in conversationSnapshot {
let memberArray = conversation.childSnapshot(forPath: "members").value as! [String]
partner = memberArray.filter {$0 != (Auth.auth().currentUser?.uid)!}
if memberArray.contains((Auth.auth().currentUser?.uid)!) {
let newPartner = (String(describing: partner))
title = newPartner.replacingOccurrences(of: "[\\[\\]\\^+<>\"]", with: "", options: .regularExpression, range: nil)
databaseRef.child("bodhi").child(title).observeSingleEvent(of: .value, with: { (snapshot) in
if let bodhiDict = snapshot.value as? [String: AnyObject]
{
//I realize that the variable is being wrongly redeclared here to make this run.
let partnerName = (bodhiDict["Name"] as! String)
print ("partnerName returned from firebase: \(partnerName)")
// Point A: This prints "Sandy"
}
})
print ("partnerName: \(partnerName)")
// Point B: This prints nothing but if I add partnerName = "Sandy", then the function complete
title = partnerName
print ("new title: \(title)")
let conversation = Conversation(conversationTitle: title, key: conversation.key, conversationMembers: memberArray, conversationMemberCount: memberArray.count)
conversationsArray.append(conversation)
}
}
handler(conversationsArray)
}
}
func createGroup(withTitle title: String, andDescription description: String, forUserIds ids: [String], handler: #escaping (_ groupCreated: Bool) -> ()) {
REF_GROUPS.childByAutoId().updateChildValues(["title": title, "description": description, "members": ids])
// need to add code here for slow internet: if successful connection true else error
handler(true)
}
func getAllGroups(handler: #escaping (_ groupsArray: [Group]) -> ()) {
var groupsArray = [Group]()
REF_GROUPS.observeSingleEvent(of: .value) { (groupSnapshot) in
guard let groupSnapshot = groupSnapshot.children.allObjects as? [DataSnapshot] else { return}
for group in groupSnapshot {
let memberArray = group.childSnapshot(forPath: "members").value as! [String]
if memberArray.contains((Auth.auth().currentUser?.uid)!) {
let title = group.childSnapshot(forPath: "title").value as! String
let description = group.childSnapshot(forPath: "description").value as! String
let group = Group(title: title, description: description, key: group.key, members: memberArray, memberCount: memberArray.count)
groupsArray.append(group)
}
}
handler(groupsArray)
}
}
}
I recommend you read the documentation on closures:
Closures
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html
Closures: Capturing Values
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID103