Search code examples
iosswiftfirebase-realtime-databaseasync-awaitgrand-central-dispatch

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?


Solution

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