You probably found yourself in a situation where you had to do a bunch of asynchronous tasks and you just wanted to get notified when all of them finish. There are two easy ways of doing this: DispatchGroup and OperationQueue. We’ll cover them both in this post.
The Story
We’ll work off one of the demo projects I have on GitHub – DADependencyInjection. When you call a method to get your movies from the API your closure will get called with the results. We’ll modify this method in this post to call the first 5 pages for the popular movies (5 API calls). After all 5 are finished and parsed, we’ll call the closure. Just to be clear, I’m not advocating you call your APIs like this, I’ll just use this as an example on how to dispatch multiple concurrent tasks and get notified when all of them finish.
DispatchGroup
If you’re working on a project that’s not using Operations and you don’t want to refactor any of the existing code, DispatchGroups is probably the easiest way for you to know when a bunch of asynchronous calls is finished.
DispatchGroup is a part of GCD and in Swift 3 we got a nice swiftified GCD. It doesn’t look like someone glued a piece of C code in the middle of your pretty Swift code. Let’s see how to use DispatchGroup:
func getMovies(onCompleted: (([MovieItem]) -> ())?) { var result: [MovieItem] = [] let dispatchGroup = DispatchGroup() for i in 1...5 { guard let urlString = DataSourceConstants.URLString(forPage: "\(i)") else { continue } dispatchGroup.enter() self.networkingProvider.restCall(urlString: urlString) { (responseObject) in guard let responseData = responseObject, let jsonObject = try? JSONSerialization.jsonObject(with: responseData, options: [JSONSerialization.ReadingOptions.allowFragments]) else { dispatchGroup.leave() return } let movies = self.moviesFactory.movieItems(withJSON: jsonObject) result = result + movies dispatchGroup.leave() } } dispatchGroup.notify(queue: DispatchQueue.global()) { onCompleted?(result) } }
Don’t mind the loop, it’s there just so I can show you how this works 🙂
As you can see, you create your DispatchGroup at the top of the function and just before you execute an async piece of code, you enter the group. In your closure, you leave the same group just before the closure completes. When your DispatchGroup is empty, a closure will be called. This is, in effect, what we want. So in this closure we’ll call our ‘onCompleted’ closure with the results.
What’s really important here is the enter-leave pairs. You have to be very careful and make sure that you leave the group. It would be easy to introduce a bug in the code above. Let’s say that we didn’t leave the group in that guard statement above, just before the return. If the API called failed, or the JSON was malformed, the number of group entries would not match the number of leaves. So the group completion handler would never get called. If you’re calling this method from the UI and displaying an activity indicator while your networking requests are running, you would never get a callback from the method, and you would keep on spinning 🙂
Other than that, DispatchGroup is pretty simple to use.
OperationQueue
Operation queues are great and all, but if you just want to know when your queue is finished you won’t find a ready-made API waiting for you. This is actually totally fine, because, operation queue is designed around a different concept, but let’s not get side tracked.
There is a simple trick you can use to get notified when your async tasks are finished. The trick is to use dependencies. You have two options here. If you need your operations to execute one after another, you can set the next operation to be dependent on the previous. So when your last operation is finished, your queue is finished as well. This is easy to do. But what if you have a bunch of concurrent operations? You want to execute as many of them as possible at the same time and you want to get notified when all of them finish?
Well, just create another operation. Operations can have dependencies on multiple operations. So when you create your operations you add them as a dependency to that operation. When all dependent operation finish, your operation will get executed. And this way you can tell that your ‘queue’ is finished. If you think about this from a logical perspective, it makes perfect sense. Anyone can add a bunch of operations in the operation queue, and if you don’t own it completely, you don’t know who added what. So having a callback that will tell you that the queue is empty would not be very useful. Dependencies are baked into the Operations, so why not use them 🙂
Let’s see some code:
func getMovies(onCompleted: (([MovieItem]) -> ())?) { operationQueue.cancelAllOperations() var result: [MovieItem] = [] let queueCompletionOperation = BlockOperation { onCompleted?(result) } var operations: [Operation] = [] operationQueue.isSuspended = true for i in 1...5 { guard let urlString = DataSourceConstants.URLString(forPage: "\(i)") else { continue } let networkingOperation = GetDataOperation(withURLString: urlString, andNetworkingProvider: networkingProvider) let parsingOperation = ParseDataOperation(withFactory: moviesFactory) networkingOperation.completionBlock = { parsingOperation.moviesData = networkingOperation.responseData } parsingOperation.completionBlock = { if let moviesArray = parsingOperation.movies { DispatchQueue.global().sync(flags: .barrier) { result = result + moviesArray } } } parsingOperation.addDependency(networkingOperation) queueCompletionOperation.addDependency(parsingOperation) operations.append(contentsOf: [parsingOperation, networkingOperation]) } operations.append(queueCompletionOperation) operationQueue.addOperations(operations, waitUntilFinished: false) operationQueue.isSuspended = false }
We have our ‘queueCompletionOperation’ at the top and in the block we’re calling our closure. We suspend the queue before adding new operations and we’re setting the dependency on our queueCompletionOperation to parsingOperation. After the loop is finished, we add all the operations in the queue and un-suspend the queue.
That’s pretty much it.
Conclusion
Operations are great, but unless your project is already using them, you’ll probably wanna go with DispatchGroup. You can also implement this into your existing codebase practically without any refactoring. Both of these approaches are just fine and they do what it says on the tin. But at the end of the day, it will be up to you and your project.
You can find the code on my GitHub repository. And as usual, have a nice day 🙂
Dejan.
What is the best option where I need to execute a series of “operations” on a dispatch or operation block queue? My task is that I need to do a bunch of “repeating sales” operations; but its important that each one is done in order. So first in, first out. Given this, would you say a block operation is the best option? Thanks
You’re right! I would go with a block operation on an operation queue. And you can set the max number of concurrent operations to ‘1’ on the queue, or you can set the dependencies of your operations to the previous operation, what ever you fancy 🙂 If you keep adding operations to the queue, maybe setting the number of concurrent operations to 1 would be the simplest way to go. You’re welcome 🙂
Hi. Can you tell me why my request doesn’t enter into completion handler all the time?
let dispatchGroup = DispatchGroup()
var index = 0
for a in inAppPurchaseIds {
index = index + 1
print(“INDEX ENTER –> \(index)”)
dispatchGroup.enter() // < YES”)
guard let me = self else {
print(“INDEX LEAVE ME –> \(a)”)
dispatchGroup.leave()
return
}
print(“DATA —- \(String(describing: data))”)
print(“INDEX LEAVE –> \(a)”)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
}
}
//———— LOG ——-
INDEX ENTER –> 1
INDEX ENTER –> 2
INDEX ENTER –> 3
LOOP COUNT –> YES
DATA —- Optional(“Product Not Purchased”)
INDEX LEAVE –> kr.co.adcasting.CCUser.proPackage
You can check here, it entered 3 times in a loop but never left first 2 times when we are doing the authentication process with apple id.
hello. Thanks for a great article.
I recently moved from DispatchGroups to OperationQueues because I needed to control how many concurrent tasks could run and the DispatchGroup alone does not have that ability, that OperationQueue has.
However I then discovered DispatchSemaphore which is great way to keep using DispatchGroups while implementing the thread control to avoid thread explosion.
Simply create a semaphore:
let itemsSemaphore = DispatchSemaphore (5) // 5 tasks max in this case
then, before calling group.enter(), call
itemsSemaphore.wait()
and before calling group.leave(), call
itemsSemaphone.signa()