I'm kind of new to programming in general, so I have this maybe simple question. Actually, writing helps me to identify the problem faster.
Anyway, I have an app with multiple asynchronous calls, they are nested like this:
InstagramUnoficialAPI.shared.getUserId(from: username, success: { (userId) in
InstagramUnoficialAPI.shared.fetchRecentMedia(from: userId, success: { (data) in
InstagramUnoficialAPI.shared.parseMediaJSON(from: data, success: { (media) in
guard let items = media.items else { return }
self.sortMediaToCategories(media: items, success: {
print("success")
// Error Handlers
Looks horrible, but that's not the point. I will investigate the Promise Kit once I get this working.
I need the sortMediaToCategories
to wait for completion and then reload my collection view. However, in the sortMediaToCategories
I have another nested function, which is async too and has a for in loop.
func sortMediaToCategories(media items: [StoryData.Items],
success: @escaping (() -> Swift.Void),
failure: @escaping (() -> Swift.Void)) {
let group = DispatchGroup()
group.enter()
for item in items {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: { group.notify(queue: .global(), execute: {
self.collectionView.reloadData()
group.leave()
}) },
failure: { print("error") })
//....
I can't afford the collection view to reload every time obviously, so I need to wait for loop to finish and then reload.
I'm trying to use Dispatch Groups, but struggling with it. Could you please help me with this? Any simple examples and any advice will be very appreciated.
The problem you face is a common one: having multiple asynchronous tasks and wait until all are completed.
There are a few solutions. The most simple one is utilising DispatchGroup
:
func loadUrls(urls: [URL], completion: @escaping ()->()) {
let grp = DispatchGroup()
urls.forEach { (url) in
grp.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
// handle error
// handle response
grp.leave()
}.resume()
}
grp.notify(queue: DispatchQueue.main) {
completion()
}
}
The function loadUrls
is asynchronous and expects an array of URLs as input and a completion handler that will be called when all tasks have been completed. This will be accomplished with the DispatchGroup
as demonstrated.
The key is, to ensure that grp.enter()
will be called before invoking a task and grp.leave
is called when the task has been completed. enter
and leave
shall be balanced.
grp.notify
finally registers a closure which will be called on the specified dispatch queue (here: main) when the DispatchGroup grp
balances out (that is, its internal counter reaches zero).
There are a few caveats with this solution, though:
For all of these caveats there are nice solutions which should be implemented utilising suitable third party libraries. For example, you can submit the tasks to some sort of "executer" which controls how many tasks run concurrently (match like OperationQueue and async Operations).
Many of the "Promise" or "Future" libraries simplify error handling and also help you to solve such problems with just one function call.