Using DispatchGroup to sequentially execute tasks in Swift

One of the most common use cases when building an app is to fetch data from a server and then display it. This can achieved by multiple means. For this blog post, we would like to query three URLs and display the data, only when all the three URLs have returned in order.

Callbacks are an easy way to achieve this. We also can use task dependency and queues to achieve this. But due to the asynchronous nature of the server calls, we won’t be able to maintain the order. The code below will prove it to you.

	func test_ASYNC_Operations_WITHOUT_DispatchQueues() {
		let activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
		activityIndicator.center = self.view.center
		activityIndicator.backgroundColor = UIColor.red
		activityIndicator.startAnimating()
		self.view.backgroundColor = UIColor.blue
		self.view.addSubview(activityIndicator)
		let dispatchDownload = DispatchDownload()
		let operationQueue = OperationQueue()
		operationQueue.addOperation({
			dispatchDownload.downloadUrls(serial: 1) { result in
				switch result {
				case .success(let data):
					print("Finished 1")
				case .failure(let error):
					print(error)
				}
			}
		})
		
		let operation2 = BlockOperation (block: {
			dispatchDownload.downloadUrls(serial: 2) { result in
				switch result {
				case .success(let data):
					print("Finished 2")
				case .failure(let error):
					print(error)
				}
			}
		})
		operationQueue.addOperation(operation2)
		
		let operation3 = BlockOperation (block: {
			dispatchDownload.downloadUrls(serial: 3) { result in
				switch result {
				case .success(let data):
					print("Finished 3")
				case .failure(let error):
					print(error)
				}
			}
		})
		operationQueue.addOperation(operation3)
		
		let operation4 = BlockOperation (block: {
			dispatchDownload.downloadUrls(serial: 3) { result in
				switch result {
				case .success(let data):
					print("Finished 4")
				case .failure(let error):
					print(error)
				}
			}
		})
		operationQueue.addOperation(operation4)
		
		operation3.addDependency(operation2)
		operationQueue.addBarrierBlock {
			print("***** adding barrier block ***** ")
		}
		operation4.addDependency(operation3)
		operationQueue.waitUntilAllOperationsAreFinished()
	}
***** adding barrier block ***** 
downloaded URL 3
Finished 4
downloaded URL 2
Finished 2
downloaded URL 3
Finished 3
downloaded URL 1
Finished 1

As you can see the order is never maintained. In order to fix this, we can use dispatch groups. You can read more about them here https://developer.apple.com/documentation/dispatch/dispatchgroup

Use group.enter to explicitly indicate that a block has entered the group and group.leave to explicitly indicate that a block in the group finished executing. You would need to use these in pairs.

Use group.wait() to wait synchronously for the previously submitted work to finish.

Use group.notify to schedule the submission of a block to a queue when all tasks in the current group have finished executing.

DispatchGroup allows you to aggregate a set of tasks and synchronize behaviors on the group. You attach multiple work items to a group and schedule them for asynchronous execution on the same queue or different queues. When all work items finish executing, the group executes its completion handler. You can also wait synchronously for all tasks in the group to finish executing.

Here is an example to download five URLs sequentially using dispatchgroup.

	func testDispatchGroup() {
		let activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
		activityIndicator.center = self.view.center
		activityIndicator.backgroundColor = UIColor.red
		activityIndicator.startAnimating()
		self.view.backgroundColor = UIColor.blue
		self.view.addSubview(activityIndicator)
		let downloadUrlPositions = [1,2,3,4,5]
		let dispatchDownload = DispatchDownload()
		let group = DispatchGroup()
		for position in downloadUrlPositions {
			group.enter()
			dispatchDownload.downloadUrls(serial: position) { result in
				switch result {
				case .success(let data):
					print("Finished \(position) \(data!.count)")
				case .failure(let error):
					print(error)
				}
				group.leave()
			}
		}
		group.wait()
		group.notify(queue: DispatchQueue.main, execute: {
			activityIndicator.stopAnimating()
			print("***** finished downloading all the items *****")
		})
	}
downloaded URL 2
Finished 2 6164
downloaded URL 3
Finished 3 6702
downloaded URL 4
Finished 4 6164
downloaded URL 5
Finished 5 6164
downloaded URL 1
Finished 1 6164
***** finished downloading all the items *****

As you can see, we use group.wait to wait until all the tasks have been completed. Once they complete, we remove the activity indicator. It’s pretty straight forward.

Now let’s apply this same logic, when we use operation queues. We have four operations which will be downloaded in order.

	func test_ASYNC_Operations_WITH_DispatchQueues() {
		let activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
		activityIndicator.center = self.view.center
		activityIndicator.backgroundColor = UIColor.red
		activityIndicator.startAnimating()
		self.view.backgroundColor = UIColor.blue
		self.view.addSubview(activityIndicator)
		let dispatchDownload = DispatchDownload()
		let operationQueue = OperationQueue()
		operationQueue.maxConcurrentOperationCount = 1
		operationQueue.addOperation({
			let group = DispatchGroup()
			group.enter()
			dispatchDownload.downloadUrls(serial: 1) { result in
				switch result {
				case .success(let data):
					print("Finished 1")
				case .failure(let error):
					print(error)
				}
				group.leave()
			}
			group.wait()
		})
		
		let operation2 = BlockOperation (block: {
			let group = DispatchGroup()
			group.enter()
			dispatchDownload.downloadUrls(serial: 2) { result in
				switch result {
				case .success(let data):
					print("Finished 2")
				case .failure(let error):
					print(error)
				}
				group.leave()
			}
			group.wait()
		})
		operationQueue.addOperation(operation2)
		
		let operation3 = BlockOperation (block: {
			let group = DispatchGroup()
			group.enter()
			dispatchDownload.downloadUrls(serial: 3) { result in
				switch result {
				case .success(let data):
					print("Finished 3")
				case .failure(let error):
					print(error)
				}
				group.leave()
			}
			group.wait()
		})
		operationQueue.addOperation(operation3)
		
		let operation4 = BlockOperation (block: {
			let group = DispatchGroup()
			group.enter()
			dispatchDownload.downloadUrls(serial: 3) { result in
				switch result {
				case .success(let data):
					print("Finished 4")
				case .failure(let error):
					print(error)
				}
				group.leave()
			}
			group.wait()
		})
		operationQueue.addOperation(operation4)
		
		operation3.addDependency(operation2)
		operationQueue.addBarrierBlock {
			print("***** adding barrier block ***** ")
			DispatchQueue.main.async {
				activityIndicator.stopAnimating()
			}
                        print("***** Finished all tasks ***** ")
		}
		operation4.addDependency(operation3)
		operationQueue.waitUntilAllOperationsAreFinished()
	}

Output is shown below

downloaded URL 1
Finished 1
downloaded URL 2
Finished 2
downloaded URL 3
Finished 3
downloaded URL 3
Finished 4
***** adding barrier block ***** 
***** Finished all tasks ***** 

As you might have noticed from the output, we download the URLs in order, and display it when all tasks finish.

class DispatchDownload {
	let searchFilterUrl = URL(string: "https://api.doordash.com/v1/consumer_search_filters/?lat=37.7833876&lng=-122.4080126")!
	init() {
	}
	func downloadUrls(serial: Int, onCompleteResult: @escaping ((Result<Data?, Error>) -> ())) {
		let task = URLSession.shared.dataTask(with: URLRequest(url: searchFilterUrl)) { data, response, error in
			guard let response = response, let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode)  else {
				return
			}
			guard error == nil else {
				return
			}
			print("downloaded URL \(serial)")
			onCompleteResult(.success(data))
		}
		task.resume()
	}
}

Leave a Reply

Your email address will not be published. Required fields are marked *