How to instantiate a class with asynchronous Fireb

2020-07-27 23:54发布

问题:

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!

回答1:

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.



回答2:

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.