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 :
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 :
It's simple to visualize and use, kudos to Apple developer team.
A few stuff to take note of
-
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. -
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()
}
-
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. -
Remember to call
.leave()
outside of thecase .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. -
In this post we mentioned
notify()
, its for asynchronous task, meaning the current thread can continue execute code after thenotify(){}
block and then go back tonotify(){}
completion handler when the dispatch group count reach 0. The alternative ofnotify()
is usingwait()
, which will block the current thread from executing the code afterwait()
until all the task of dispatch group is done (ie. count reaches 0).