Handle Multiple Async Callbacks with DispatchGroup

I remember the first time I heard of a DispatchGroup - it was a few years into my iOS career and my teammate mentioned that he used it in his pull request code. He said this DispatchGroup made it easy to wait for multiple async callbacks to complete before doing some additional work. I wondered how I had never heard of this before. Let’s take a look at what he meant with a concrete code example.

Let’s say I have 3 different callbacks and want to do something after all 3 have finished. We might have some initial code that looks like this:


let api = API()

api.getUsername(for: USER_ID) { username in
    // save username somewhere
}

api.getHighScore(for: USER_ID) { highScore in
    // save high score somewhere
}

api.getFriendNames(for: USER_ID) { friendNamesArray in
    // save friend names somewhere
}

// Display user profile once we have all the info above

If you’ve ever worked on something like this, maybe you’ve asked yourself what the cleanest solution is to ensure that all the callbacks are run before you do any more work. Over the years I’ve had several solutions come to mind: 

1. We could create boolean variables that correspond to each callback, each with an initial value of false. When a callback runs, set its corresponding variable to true & then check to see if all the “callback completed” variables are true. If none are false, then we know all the callbacks have run. This works, but it can get messy or impossible as more callbacks are added or if you have an unknown number of callbacks to manage (maybe you loop over an array of users & need to get info for each of them). This solution is starting to feel like the wrong one.

2. Another possibility would be to store an Int type variable to act as a counter. It will initially be zero, and we’ll increment it right before each API call is started. Then, in each callback, decrement the value and check if it’s back to zero - when the counter gets back down to zero, you know that all callbacks have completed. This approach also works & allows us to keep track of an infinite number of callbacks. But did you know that Apple has already abstracted this logic into a simple object called DispatchGroup?

To use a dispatch group, simply initialize one and call the enter() method before each API call, then in each callback call leave() (you’ll most likely want to call leave() at the end of the callback code, after you’ve stored any data returned). The last step is to set up the dispatch group’s callback with notify(qos:flags:queue:work:) so you can do whatever work is needed after all callbacks have completed. Here’s our initial code from above with a DispatchGroup implemented:


let api = API()
let dispatchGroup = DispatchGroup()
    
dispatchGroup.enter()
api.getUsername(for: USER_ID) { username in
    // save username somewhere
    dispatchGroup.leave()
}

dispatchGroup.enter()
api.getHighScore(for: USER_ID) { highScore in
    // save high score somewhere
    dispatchGroup.leave()
}

dispatchGroup.enter()
api.getFriendNames(for: USER_ID) { friendNamesArray in
    // save friend names somewhere
    dispatchGroup.leave()
}

dispatchGroup.notify(queue: .main) {
    // Display user profile info
}

I love the simplicity Apple has given us here - we simply tell the DispatchGroup when async blocks have started and finished, and define work to be done afterwards (being careful to think about which queue it should be run on 😅). Back when I learned about this, I couldn’t believe it took so long for me to hear about them. Then again Apple has given us quite a few tools & I’m not sure how one would learn about all of them except by reading through the entire standard library…

In any case, I hope this article was enlightening and helpful - hopefully you were able to learn about the helpful DispatchGroup class earlier in your career than I did! Until next time ✌🏼