Charles Black
Charles Black

Reputation: 143

Swift: How do I call this completion block only after code executes?

I am having trouble understanding why this code is executing the completion block before users are appended to a category.

First I am trying to fetch the category. Each category has an array of user Id's, which are looped over to fetch a user from a separate location in my database.

Here is the Firebase data:

["Category 1": {
title = lifestyle;
users =     (
    ESKYpDMPiHW34,
    HJ8ItJDoExZMQ,
    1WDnoPy4PeQkm
);
}, "Category 2": {
title = fitness;
users =     (
    ESKYpDMPiHW3,
    HJ8ItJDoExZM,
    1WDnoPy4PeQk
);
}, "Category 3": {
title = health;
}]

Here is my code:

class func fetchFeaturedUsers(completion: @escaping ([UserCategory]) -> Swift.Void) {
    var categories = [UserCategory]()

    let dbRef = Database.database().reference().child("categories")
    dbRef.observeSingleEvent(of: .value, with: { (snap) in

        if let dict = snap.value as? [String: Any] {
            for (_, value) in dict {

                if let category = value as? [String:Any] {

                    let title = category["title"] as? String
                    let newCategory = UserCategory(title: title?.capitalized, users: [User]())
                    categories.append(newCategory)

                    if let users = category["users"] as? [String] {
                        for id in users {
                            User.fetchUser(userId: id, completion: { (newUser) in
                                newCategory.users?.append(newUser)
                            })
                        }
                    }
                }
            }
        }
        DispatchQueue.main.async {
            completion(categories)
        }
    })
}

Upvotes: 1

Views: 291

Answers (1)

Alexandre Lara
Alexandre Lara

Reputation: 2568

The problem is that your User.fetchUser method is an asynchronous call that is being done in another thread. To solve that you can create a DispatchGroup that will wait until all calls be completed before calling completion like this:

class func fetchFeaturedUsers(completion: @escaping ([UserCategory]) -> Swift.Void) {
    var categories = [UserCategory]()
    let dispatchGroup = DispatchGroup()

    let dbRef = Database.database().reference().child("categories")
    dbRef.observeSingleEvent(of: .value, with: { (snap) in

        if let dict = snap.value as? [String: Any] {
            for (_, value) in dict {

                if let category = value as? [String:Any] {

                    let title = category["title"] as? String
                    let newCategory = UserCategory(title: title?.capitalized, users: [User]())
                    categories.append(newCategory)

                    if let users = category["users"] as? [String] {
                        for id in users {
                            dispatchGroup.enter()
                            User.fetchUser(userId: id, completion: { (newUser) in
                                newCategory.users?.append(newUser)
                                dispatchGroup.leave()
                            })
                        }
                    }
                }
            }
        }

        dispatchGroup.notify(queue: DispatchQueue.global(qos: .background)){
            completion(categories)                
        }
    })
}

You can read more about DispatchGroup in the documentation.

Upvotes: 1

Related Questions