Sergio Charles
Sergio Charles

Reputation: 379

How to use dispatch group to asynchronously await Firebase callback upon class initialization?

I am instantiating a User class via a Firebase DataSnapshot. Upon calling the initializer init(snapshot: DataSnapshot), it should asynchronously retrieve values from two database references, namely pictureRef and nameRef, via the getFirebasePictureURL and getFirebaseNameString methods' @escaping completion handlers (using Firebase's observeSingleEvent method). The references pictureRef and nameRef are both children of a single parent node. However, when instantiating the class, it never initializes the name and picture User class properties because init is executed synchronously.

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

        })
     }
  }

It was suggested in a previous post that I add an @escaping completion handler to the init method:

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

However, this would require that if I initialize this class via User(snapshot: snapshot) in a method outside of this class that I encapsulate the body of that method within the completion handler of the User init method, which wouldn't work for my current project.

Is there a way to employ dispatch groups to pause execution on the main thread until the fullName and pictureURL are populated with values? Or is there an alternative way of doing this?

Upvotes: 1

Views: 368

Answers (1)

Rob
Rob

Reputation: 437872

Is there a way to employ dispatch groups to pause execution on the main thread until the fullName and pictureURL are populated with values? Or is there an alternative way of doing this?

Well, you never want to “pause” execution. But, you can use dispatch groups to notify your app when two asynchronous methods are complete and when it’s now possible to create that new instance:

func createUser(for userId: String, completion: @escaping (User) -> Void) {
    var pictureUrl: URL?
    var fullName: String?

    let group = DispatchGroup()

    group.enter()
    getFirebaseNameString(userId: userId) { name in
        fullName = name
        group.leave()
    }

    group.enter()
    getFirebasePictureURL(userId: userId) { url in
        pictureUrl = url
        group.leave()
    }

    group.notify(queue: .main) {
        guard
           let pictureUrl = pictureUrl, 
           let fullName = fullName 
        else { return }

        completion(User(uid: userId, fullName: fullName, pictureURL: pictureUrl))
    }
}

And then:

let userId = ...
createUser(for: userId) { user in 
    // use `User` instance here, e.g. creating your new node
}

Where User is now simplified:

class User {
    let uid: String
    let fullName: String
    let pictureURL: URL

    init(uid: String, fullName: String, pictureURL: URL) {
        self.uid = uid
        self.fullName = fullName
        self.pictureURL = pictureURL
    }
}

But I’d advise against trying to bury asynchronous code inside the init method. Instead, I'd flip it around and create your instance when the two asynchronous methods are done.

Upvotes: 1

Related Questions