<– Back

Using Completion Handlers & Dispatch Groups to manage Networking calls.

Here, I’ll be using Alamofire to make network calls. If you’re not familiar with it, there’s nothing to worry about. You can read about it here. If you want to use URLSessions, you can go ahead with that too. Most of the code is built around network calls, but the topics I’ll be explaining help you handle them, you won’t need any advanced knowledge about network calls here.

Completion Handlers

Completion Blocks may seem weird at first but are very handy and easy to use. By my understanding, a completion handler means:

Do stuff when something is done

One could say they are a type of closure. Closures are just pieces of code that can be thrown around anywhere in your code.

let closure = { (name:String) -> String in
	return "Hello, \(name)!"
}

let message = closure("Igor")
print(message)

For example, say you’re making a request to get an array of tasks from the server for your to-do app and want it to be stored in a local array in your code. Your code would look something like:

Alamofire.request(serverURL).responseData {
	response in

	if let data = response.result.value {
	    self.todos = data
	}
}

Here, .responseData is a function chained to the web request and only one parameter is provided to it, a closure. Inside it, we get the array from the respone and assign it to our variable. Since this closure is executed after the web request is completed, it is a completion handler.
Whenever a function with a completion handler finishes (the isFinished property of the function changes to true), the completion handler code is executed. It is very useful when dealing with functions which can take quite a bit of time, such as network calls.

Declaring a function with a completion handler

Declaring a function with a completion handler is easy. Just add it as another parameter but the syntax is a little different.

private func getImageFromServer(parameters: String, completion: @escaping (String) -> Void) {
    
    // add your code here
    Alamofire.request(imageURL).responseData { (response) in

		if let data = response.result.value {
		    // do stuff with data
            // maybe set it to your UILabel
            self.titleLabel.text = data
		    // use this where the function's job is done, in this case, image has been set to the imageView
    		completion("Finished setting the image.")
		}
	} 
    
}

This is not a good example of how you should be handling responses from web requests. If you want to learn more about that, click here.

completion: @escaping (String) -> Void

(String) means the closure takes a String as a parameter for it’s code. Void means it returns nothing. @escaping means the closure will be executed after the function completes execution. By default, all closures are non-escaping, i.e., they are called within the function.

Calling functions with a completion handler

Now that you have successfully declared the function, here’s how you can call it. Initially, Xcode may auto-complete it to:

getImageFromServer(parameters: [String], completion: [(String) -> Void])

Here, [String] and [(String) -> Void] are parameters. You can fill in the all parameters except the completion one as you would. Just press return after selecting the completion parameter to get a code block. The code block will look like this:

getImageFromServer(parameters: stringLikeABee) { (stringReceived) in
	// code
	print(stringReceived)
}

print(stringReceived) is executed after the function finishes execution.

If the function encounters an error such that it never got inside the if block. completion() is never executed, therefore it never prints “Finished setting the image.” The function just returns, without executing the completion block.

Dispatch Groups

When we have to keep track of multiple function calls and track all of them, we use Dispatch Groups. It has three main functions: .enter, .leave, .notify. For better understanding, assume that a dispatch group has a count variable which tracks the number of .enter calls and .leave calls. It only executes .notify when count == 0. .enter() increments the counter by 1 and .leave() decrements it by 1.

let dispatchGroup = DispatchGroup()

// count = 0

dispatchGroup.enter() // count = 1
dispatchGroup.enter() // count = 2
dispatchGroup.leave() // count = 1
dispatchGroup.enter() // count = 2
dispatchGroup.leave() // count = 1
dispatchGroup.leave() // count = 0, bingo

// I'm using the main thread here, but you can use any thread.
// Main threads are generally reserved for UI changes.
// I'll explain a little about threads and multithreading below.

dispatchGroup.notify(queue: .main) {
	// executed when count comes back to 0
}

DispatchGroup allows for aggregate synchronization of work. You can use them to submit multiple different work items and track when they all complete, even though they might run on different queues. This behavior can be helpful when progress can’t be made until all of the specified tasks are complete.
- Apple Developer Documentation on Dispatch Groups

In simpler terms, Dispatch Groups are used to track multiple “work items.” By “might run on different queues,” they mean that the multiple “work items” may be executed simultaneously by the processor. This concept is called multithreading, different bits of code are executed simultaneously on “threads”.

Now let’s see how we can use dispatch groups in our code. In this example, let’s say we have to make two web requests and only when both requests are complete, we want to run a block of code.
First we need to declare a dispatch group in our file.

let dispatchGroup = DispatchGroup()

The second step is to declare two functions, each with one web request. We make a .enter() call to increment count by 1 and tell the dispatch group that it has one task pending in each function. After the web request completes, .responseData() is executed and in that, we call our completion handler. Here’s how the code will be executed:

Assume we have a networking() function to call both functions containing web requests.

networking() -> firstFunction() & secondFunction() simultaneously -> .enter() call is made in each function -> on completion of each function: code goes back and a .leave() call is made inside the completion block to tell the Dispatch Group that the respective function has completed execution.

Here are the two functions:

private func getFirstImage(parameters: String, completion: @escaping (Int) -> Void {

    // we call .enter() here, we'll add a .leave() call in the completion block.
    // this way, count will be balanced only when the function completes successfully.
    // you may need to add completion() in multiple places in complex functions to handle all cases.
    
    dispatchGroup.enter()

    // we make a web request using Alamofire's request function
    // we chain a function called .responseData which accepts a completion handler as it's parameter
    // in that completion handler, we use the response we got from the web request
    // to do whatever we want

    Alamofire.request(imageURL).responseData { (response) in

		if let data = response.result.value {
		    self.firstImageView?.image = UIImage(data: data)
		    // image has been set to the imageView, work is complete
    		completion("Finished setting the first image.")
		}

	} 
    
}


private func getSecondImage(parameters: String, completion: @escaping (Int) -> Void {
    
    dispatchGroup.enter()

    Alamofire.request(imageURL).responseData { (response) in

		if let data = response.result.value {
		    self.secondImageView?.image = UIImage(data: data)
			// image has been set to the imageView, work is complete
    		completion("Finished setting the second image.")
		}

	} 
    
}

Now that our web requests complete, we call them in a separate function called networking. In each web request function’s completion block, we add dispatchGroup.leave() to balance our count variable.

Note: It is important that you balance all .enter calls with a .leave call, otherwise it would result in a non-zero count and .notify will never be called.

private func networking(parameters: String, completion: @escaping (Int)->()) {
    
    // make networking calls here
    
    getFirstImage(parameters: parameters) { (imageSetString) in
        // imageSetString is the string sent by the completion block of the function
        // first image set
        print(imageSetString)
        // balance the .enter() call inside the function
        self.dispatchGroup.leave()
    }
    
    getSecondImage(parameters: parameters) { (imageSetString) in
        // imageSetString is the string sent by the completion block of the function
        // second image set
        print(imageSetString)
        // balance the .leave() call inside the function
        self.dispatchGroup.leave()
    }

}

Now we add .notify. It will only be called when both the completion handlers complete execution of their respective .leave function call. We know that each completion handler’s .leave will only execute if completion(string) is executed in web request functions, only when they are executed successfully. Basically, when both images are retrieved successfully, the notify function’s closure will be executed. Here, as you can notice, the notify function is executed on the main thread (Again, the main thread should only be used for UI updates such as reloading your view controller).

dispatchGroup.notify(queue: .main) {
	// code
    print("Both images retrieved")
    completion("Networking done.")
}

Here’s the whole networking() function:

private func networking(parameters: String, completion: @escaping (Int)->()) {
    
    // make networking calls here
    
    getFirstImage(parameters: parameters) { (imageSetString) in
        // imageSetString is the string sent by the completion block of the function
        // first image set
        print(imageSetString)
        // balance the .enter() call inside the function
        self.dispatchGroup.leave()
    }
    
    getSecondImage(parameters: parameters) { (imageSetString) in
        // imageSetString is the string sent by the completion block of the function
        // second image set
        print(imageSetString)
        // balance the .leave() call inside the function
        self.dispatchGroup.leave()
    }

    dispatchGroup.notify(queue: .main) {
    	// code
        print("Both images retrieved")
        completion("Networking done.")
    }

}

You can find the whole code here. Feel free to contact me wherever and whenever if you have any doubts.

iOS Swift