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!
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.
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.