Let say you have two different network call function, one to get list of users and one to get list of avatars :

// Get List of Users
Alamofire.request("https://httpbin.org/get?users").validate().responseJSON { response in
    switch response.result {
    case .success:
        print("Get User Successful")
        // put user into users array
		...
		
    case .failure(let error):
        print(error)
    }
}

// Get List of Avatars
Alamofire.request("https://httpbin.org/get?avatars").validate().responseJSON { response in
    switch response.result {
    case .success:
        print("Get Avatars Successful")
        // put avatar into avatars array
		...
		
    case .failure(let error):
        print(error)
    }
}

And you want to update the tableview showing users and avatars only after BOTH network call has finished, how do you do it?

Placing tableView.reloadData() in completion handler of either network call doesn't work as the other network call might not finish when the current one finish, making the tableView looks incomplete :

// Get List of Users
Alamofire.request("https://httpbin.org/get?users").validate().responseJSON { response in
    switch response.result {
    case .success:
        print("Get User Successful")
        // put user into users array
		...
        tableView.reloadData()
        // what if get avatar request havent returned yet? 
        // the table view would look empty without avatar
		
    case .failure(let error):
        print(error)
    }
}

How do you perform tableView.reloadData() only after BOTH http request is finished?

Answer : use a Dispatch Group

Apple introduced Dispatch Group in iOS 7.0 to allow us to track when multiple asynchronous tasks have completed.

We can use dispatch group to perform a function after both network request has completed like this :

// Create a dispatch group
let userListDispatchGroup = DispatchGroup()

// Get List of Users
userListDispatchGroup.enter()
Alamofire.request("https://httpbin.org/get?users").validate().responseJSON { response in

    switch response.result {
    case .success:
        print("Get User Successful")
        // put user into users array
		...
		
    case .failure(let error):
        print(error)
    }
    
    // leave the dispatch group after the network request is complete and data is parsed
    userListDispatchGroup.leave()
}

// Get List of Avatars
userListDispatchGroup.enter()
Alamofire.request("https://httpbin.org/get?avatars").validate().responseJSON { response in
	
    switch response.result {
    case .success:
        print("Get Avatars Successful")
        // put avatar into avatars array
		....
		
    case .failure(let error):
        print(error)
    }
    
    // leave the dispatch group after the network request is complete and data is parsed
    userListDispatchGroup.leave()
}

// after both network request complete, code inside this closure will be called
// queue: .main means the main queue, always use main queue to update UI
userListDispatchGroup.notify(queue: .main) {
	print("Both get users and get avatars has completed đź‘Ś")
	self.tableView.reloadData()
}

The console log looks like this when the code is executed :
Tableview update after both network request is done

Pretty neat huh? Adding few lines of dispatchGroup.enter(), dispatchGroup.leave() and dispatchGroup.notify() gets the job done easily.

We will explain how dispatch group works in very simplified terms below.

How Dispatch Group works

Imagine there's an integer variable count inside a DispatchGroup object and its value is set to 0 when you initialize it. The count is used to keep track how many tasks are pending.

When you call .enter(), the count increase by 1, as 1 task is added.
When you call .leave(), the count reduce by 1, as 1 task is finished.

When the count reaches 0, meaning all task is finished, its .notify() method will be called and the closure inside will be executed.

For the Get Users and Get Avatars request we mentioned above, the dispatch group state will look like this :
Dispatch Group Chart

It's simple to visualize and use, kudos to Apple developer team.

A few stuff to take note of

  1. You don't necessary have to use main queue for dispatchGroup.notify(queue:), you can use any of the DispatchQueue like background queue etc, just that to update UI usually we will use the main queue.

  2. If the .notify() method is placed before all of the .enter() method, the notify completion handler will be executed before we call .enter(), because at that point (when CPU follow line by line until the .notify() part) the count of the dispatch group is zero. eg:

let dispatchGroup = DispatchGroup()

dispatchGroup.notify(queue: .main) {
    print("oops already called notify because at this point of code execution flow, the count of dispatch group is 0")
}

// oh shit already executed notify before entering dispatch group
dispatchGroup.enter()
Alamofire.request("https://httpbin.org/get?avatars").validate().responseJSON { response in
    dispatchGroup.leave()
}

dispatchGroup.enter()
Alamofire.request("https://httpbin.org/get?users").validate().responseJSON { response in
    dispatchGroup.leave()
}


  1. Be sure to balance out the number of .enter() and .leave() (ie. number of enter and leave must be same) in your code, if not, the app might crash.

  2. Remember to call .leave() outside of the case .success , if you only call .leave() when the network call is successful, the dispatch group will never be finished if one or more of the network request fail. Refer 3.

  3. In this post we mentioned notify(), its for asynchronous task, meaning the current thread can continue execute code after the notify(){} block and then go back to notify(){} completion handler when the dispatch group count reach 0. The alternative of notify() is using wait(), which will block the current thread from executing the code after wait() until all the task of dispatch group is done (ie. count reaches 0).