How to instantiate a class with asynchronous Firebase methods in Swift? - 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.

Related

I cannot update the value when I fetch the user data from firebase database

I can fetch the user data from the firebase, but I cannot input the user data to a global variable.
Database.database().reference().child("users").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let username = value?["username"] as? String ?? ""
let userDigit = value?["lastDigit"] as? String ?? ""
let userDate = value?["babyDate"] as? String ?? ""
print(username)
print(userDigit)
print(userDate)
self.user.userName = username
self.user.userDigit = userDigit
self.user.userDate = userDate
}) { (error) in
print(error.localizedDescription)
}
print(self.user.userName)
print(self.user.userDigit)
print(self.user.userDate)
From the above code, I can see the username, userDigit, userDate, however, after i input the data into user class that contains 3 string variables, i cannot print the user class value.
To be specific,
print(self.user.userName)
this code returns nil.
How can I input the data?
The value of self.user.userName is nil because you're calling it outside the observer closure.
Printing a value after assigning in a closure doesn't guarantee that it will be available immediately afterwards.
Because the closure of observeSingleEvent gets called after the data is fetched from firebase.
You should modify your logic and try to access the value of user after the data has successfully been fetched from firebase.
below is a sample code that might help you understand the concept:
func getUser(with userID: String, completion: #escaping ((_ user: User) -> Void)) {
Database.database().reference().child("users").child(userID).observeSingleEvent(of: .value) { (snapshot) in
// Get user value
let value = snapshot.value as? [String: Any]
let username = value?["username"] as? String ?? ""
let userDigit = value?["lastDigit"] as? String ?? ""
let userDate = value?["babyDate"] as? String ?? ""
var user = User()
user.userName = username
user.userDigit = userDigit
user.userDate = userDate
DispatchQueue.main.async {
completion(user)
}
}
}
In the above method, we fetch data from firebase and return it inside a closure of our own once the data is available.
and then you call the above function like this:
self.getUser(with: userID!) { (user) in
// the user object inside the closure will be available after fetching data from firebase
// the user object can even be assigned to a global property
self.globalUser = user
print(self.globalUser.userName)
print(self.globalUser.userDigit)
print(self.globalUser.userDate)
}
Hope this helps

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
}

Getting values from Firebase snapshot in Swift

Im successfully getting data from Firebase but I can't manage to push it into array to use. My database is as follows:
users
-Wc1EtcYzZSMPCtWZ8wRb8RzNXqg2
-email : "mike#gmail.com"
-lists
-LJiezOzfDrqmd-hnoH-
-owner: Wc1EtcYzZSMPCtWZ8wRb8RzNXqg2
-LJif-UgPgbdGSHYgjY6
-owner: Wc1EtcYzZSMPCtWZ8wRb8RzNXqg2
shopping-lists
-LJh6sdBJtBCM7DwxPRy
-name: "weekly shopping"
-owner: "mike#gmail.com"
I have a home page after login that shows existing shopping lists on table if they exist. On viewDidLoad() I get shopping list IDs from the user and use those IDs as a reference to get details from shopping-lists.
However, I cant manage to save these data into an array as it gets deleted after closure. How can I do that in a clean way?
override func viewDidLoad() {
super.viewDidLoad()
SVProgressHUD.show()
tableView.allowsMultipleSelectionDuringEditing = false
// Sets user variable - must have
Auth.auth().addStateDidChangeListener { auth, user in
guard let user = user else { return }
self.user = User(authData: user)
// If new user, write into Firebase
self.usersRef.observeSingleEvent(of: .value, with: { snapshot in
if !snapshot.hasChild(self.user.uid) {
self.usersRef.child(user.uid).setValue(["email": user.email!])
}
})
// Get shopping lists data from "users/lists"
self.usersRef.child(user.uid).child("lists").observe(.value, with: { snapshot in
// Get list IDs
if snapshot.exists() {
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
self.listNames.append(child.key)
}
}
}
// Use list IDs - to get details
for item in self.listNames {
let itemRef = self.shoppingListsRef.child(item)
itemRef.observeSingleEvent(of: .childAdded, with: { (snapshot) in
if let value = snapshot.value as? [String: Any] {
let name = value["name"] as? String ?? ""
let owner = value["owner"] as? String ?? ""
let shoppingList = ShoppingList(name: name, owner: owner)
self.items.append(shoppingList)
}
})
}
})
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
(the question is a bit unclear so several parts to this answer to cover all possibilities. This is Swift 4, Firebase 4/5)
You don't really need to query here since you know which nodes you want by their key and they will always be read in the in order of your listNames array. This assumes self.listNames are the keys you want to read in.
for item in listNames {
let itemRef = shoppingListsRef.child(item)
itemRef.observe(.value, with: { (snapshot) in
if let value = snapshot.value as? [String: Any] {
let name = value["name"] as? String ?? ""
let owner = value["owner"] as? String ?? ""
print(name, owner)
}
})
}
Generally, queries are used when you are searching for something within a node - for example if you were looking for the node that contained a child name of 'weekly shopping'. Other than that, stick with just reading the nodes directly as it's faster and has less overhead. Keep reading...
I also removed the older NSDictionary and went with the Swift [String: Any] and modified your error checking
However, the real issue is reading that node with an .observe by .value. Remember that .value reads in all children of the node and then the children need to be iterated over to get each separate DataSnapshot. Also, .observe leaves an observer on the node notifying the app of changes, which I don't think you want. So this will answer the question as posted, (and needs better error checking)
for item in listNames {
let queryRef = shoppingListsRef
.queryOrdered(byChild: "name")
.queryEqual(toValue: item)
queryRef.observe(.value, with: { (snapshot) in
for child in snapshot.children { //even though there is only 1 child
let snap = child as! DataSnapshot
let dict = snap.value as! [String: Any]
let name = dict["name"] as? String ?? ""
let owner = dict["owner"] as? String ?? ""
print(name, owner)
}
})
}
And the answer...
This is probably more what you want...
for item in listNames {
let queryRef = shoppingListsRef
.queryOrdered(byChild: "name")
.queryEqual(toValue: item)
queryRef.observeSingleEvent(of: .childAdded, with: { snapshot in
let dict = snapshot.value as! [String: Any]
let name = dict["name"] as? String ?? ""
let owner = dict["owner"] as? String ?? ""
print(name, owner)
})
}
note the .childAdded instead of .value which presents the snapshot as a single DataSnapshot and doesn't need to be iterated over and the .observeSingleEvent which does not leave an observer attached to each node.
Edit
Based on additonal information, it would be best too change the structure to this
shopping-lists
-LJh6sdBJtBCM7DwxPRy
-name: "weekly shopping"
-uid: "Wc1EtcYzZSMPCtWZ8wRb8RzNXqg2"
and then when the user logs in just query the shopping lists node for any uid that's theirs.

Create an object from firebase database

I'm using firebase to store user records. When a user logs in, I am trying to pull the record and create a user object to pass around amongst the view controllers as opposed to hitting the database multiple times.
class User: NSObject {
var name: String?
var email: String?
}
I have a variable, myUser: User? in my controller and would like to assign the record retrieved from firebase to that variable.
func retrieveUserWith(uid: String) {
Database.database().reference().child("users").child(uid).observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
let user = User()
user.name = dictionary["name"] as? String
user.email = dictionary["email"] as? String
self.myUser = user
}
})
}
Now I understand that the firebase call is asynchronous and I can't directly assign the created user to myUser variable as shown above.
Is there another way to assign user to myUser to avoid hitting the database every time I switch view controllers?
This is not really the correct way to get the info you want. Firebase already offers a sharedInstance of the User.
if let user = Auth.auth().currentUser {
let name = user.displayName // should check that this exists
let email = user.email // should check that this exists
}
Nonetheless, to achieve this the way you are looking to do so:
class User: NSObject {
var name: String
var email: String?
static var sharedInstance: User!
init(name: String, email: String) {
self.name = name
self.email = email
}
}
Calls to firebase are asynchronous, so you should have a completionHandler that will get called when the call is finished:
func retrieveUserWith(uid: String, completionHandler: #escaping () -> ()) {
Database.database().reference().child("users").child(uid).observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
let name = dictionary["name"] as? String ?? ""
let email = dictionary["email"] as? String ?? ""
let user: User = User(name: name, email: email)
User.sharedInstance = user
completionHandler()
}
})
}
Then to use the sharedInstance of the user elsewhere in your app you can do the following:
if let user = User.sharedInstance {
// do stuff here
}

Could not cast value of type '__NSDictionaryM' to custom User Class using new Firebase

I'm attempting to query for a specific user using the new Firebase like so:
DataService.sharedInstance.REF_USERS.queryOrderedByChild("username").queryEqualToValue(field.text).observeSingleEventOfType(.Value, withBlock: { (snapshot) in
if let userDoesNotExist = snapshot.value as? NSNull {
print("No User found!")
} else {
let theUsr = snapshot.value as! User
print(theUsr)
}
}, withCancelBlock: { (error) in
// Error
})
From there I was looking to store the snapshot into its own object and access its values from there. I was attempting to do so by doing this:
class User {
let key: String!
let ref: FIRDatabaseReference?
var username: String!
var email: String!
// Initialize from data snapshot
init(snapshot: FIRDataSnapshot) {
key = snapshot.key
email = snapshot.value!["email"] as! String
username = snapshot.value!["username"] as! String
ref = snapshot.ref
}
func toAnyObject() -> AnyObject {
return [
"email": email,
"username": username,
]
}
}
The problem I'm running into is a crash with the following:
Could not cast value of type '__NSDictionaryM' (0x10239efc0) to 'Scenes.User' (0x100807bf0).
How do I fix this? I don't remember running into this problem on the old Firebase. I need access to the snapshot.value's key and for some reason can't access it by using snapshot.value.key without getting a crash so I figure I'd try passing all the data into it's own object.
snapshot.value isn't a User, it's a dictionary (*)
Here's a couple of options - there are many.
let aUser = User()
aUser.initWithSnapshot(snapshot)
then, within the User class, map the snapshot.values to the user properties
or
let aUser = User()
aUser.user_name = snapshot.value["user_name"] as! String
aUser.gender = snapshot.value["gender"] as! String
Oh, and don't forget that by using .Value your snapshot may return multiple child nodes so those will need to be iterated over
for child in snapshot.children {
let whatever = child.value["whatever"] as! String
}
In your case it's just one user so it's fine they way you have it.
*It could be
NSDictionary
NSArray
NSNumber (also includes booleans)
NSString