Trouble using child snapshot inside for loop - swift

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
}

Related

Turning a firebase database value into a variable Swift

I have a var declared and I can retrieve the value from Firebase Database but when I then print the var in ViewDidLoad, it is empty, I don't understand what's wrong. Thanks everyone
This is the answer I get when I print the var : this is the language
//
var language: String = ""
//
func getUserLanguage(completion:((String) -> Void)?) {
let ref = Database.database().reference()
let uid = Auth.auth().currentUser!.uid
ref.child("users").child(uid).observe(.value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
let languageResult = value?["language"] as? String ?? ""
completion?(languageResult)
}
)}
//
getUserLanguage { (languageResult) in
self.language = languageResult
}
// I print the value in ViewDidload
print("this is the language\(self.language)")
Try to print languageResult in function - maybe you don't get it inside the function and the variable is not assigned
Make language variable public
I think you are missing the asynchronous nature of your code here. The getUserLanguage function will call the completion only when it gets callback from observe(.value, with: { (snapshot) method of firebase. It is asynchronous. You won't get the value of language right after you call getUserLanguage in viewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
getUserLanguage { (language) in
print(language) // --> prints the expected value
self.language = language
}
print(language) // --> prints ""
}
func getUserLanguage(completion: #escaping (String) -> Void) {
let ref = Database.database().reference()
let uid = Auth.auth().currentUser!.uid
ref.child("users").child(uid).observe(.value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
let languageResult = value?["language"] as? String ?? ""
print("language: ", languageResult) // --> prints the expected value
completion(languageResult)
})
}

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.

How to use flatmap to store snapshot data into a variable

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

How can I pick three random elements out of a dictionary in Swift 4.1

I am having a problem picking three random elements out of a dictionary.
My dictionary code:
query.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let childSnap = child as! DataSnapshot
var dict = childSnap.value as! [String: Any]
}
})
You can use an array if keys are integers.
if you want to use a dictionary only then below mentioned code might be helpful for you
var namesOfPeople = [Int: String]()
namesOfPeople[1] = "jacob"
namesOfPeople[2] = "peter"
namesOfPeople[3] = "sam"
func makeList(n: Int) -> [Int] {
print(namesOfPeopleCount)
return (0..<n).map { _ in namesOfPeople.keys.randomElement()! }
}
let randomKeys = makeList(3)
You can try this for older version Of Swift where randomElement() is not available
let namesOfPeopleCount = namesOfPeople.count
func makeList(n: Int) -> [Int] {
return (0..<n).map{ _ in Int(arc4random_uniform(namesOfPeopleCount)
}
#Satish answer is fine but here's one which is a bit more complete and selects a random user from a list of users loaded from Firebase ensuring a user is only selected once.
We have have an app with two buttons
populateArray
selectRandomUser
and we have a UserClass to store our user data for each user.
class UserClass {
var uid = ""
var name = ""
init(withSnapshot: DataSnapshot) {
let dict = withSnapshot.value as! [String: Any]
self.uid = withSnapshot.key
self.name = dict["Name"] as! String
}
}
and an array to store the users in
var userArray = [UserClass]()
When the populateArray button is clicked this code runs
func populateArray() {
let usersRef = self.ref.child("users")
usersRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let snap = child as! DataSnapshot
let user = UserClass(withSnapshot: snap)
self.userArray.append(user)
}
print("array populated")
})
}
and then to select a random user use this code.
func selectRandomUser() {
if let someUser = userArray.randomElement() {
print("your random user: \(someUser.name)")
let uid = someUser.uid
if let index = userArray.index(where: { $0.uid == uid } ) {
userArray.remove(at: index)
}
} else {
print("no users remain")
}
}
This code ensures the same user is not selected twice. Note that this is destructive to the array containing the users so if that's unwanted, make a copy of the array after it's populated and work with that.

Problems Retrieving Data from Firebase in Swift 3

I'm trying to retrieve data from firebase, store it in an array which will then be used as a reference for another firebase query. Unfortunately, I can't seem to get it to work properly. The print function at the bottom is always empty. I have tried to call each successive function after the for loops, but I'm probably doing it wrong.
I have 2 branches in firebase that looks a bit like this:
-id
|
Group1
|
ID01: Created on 01.01.16
-idDetails
|
ID01
//name: name01
//description: description01
This is my code:
var array = [String]()
var id = [String]()
var items = [Item]() //Item Class Array
override func viewDidLoad() {
super.viewDidLoad()
getIdFromFirebase()
}
//first task
func getIdFromFirebase(){
for index in array //index is being used as part of path query
{
FireDbase_Main.child("Group1").child("id").child(index).observe(FIRDataEventType.childAdded, with: { (snapshot) in
self.id = (snapshot.key)
id.append(id)
})
getDataForItems()
}
}
//second task
func getDataForItems() {
let newItem = Item()
for index in id //i is being used as part of path query
{
FireDbase_Main.child("idDetails").child(index).observeSingleEvent(of: .value, with: { (snapshot) in
newItem.itemID = snapshot.key
newItem.name = (snapshot.value as! NSDictionary)["name"] as! String
newItem.desc = (snapshot.value as! NSDictionary)["description"] as! String
self.items.append(newItem)
}
printItemsArray(
}
//3rd Task: For now I have put a print here just as a placeholder task.
func printItemsArray(){
print(items.map {$0.itemID})
print(items.map {$0.name})
print(items.map {$0.desc})
}