Here's the setup. I have a method that has a completion block in which I want to return a list of Item
s. These Item
s are fetched from an API. I'd like to have each of the fetches happen asynchronously but ultimately return the Item
s all together.
Here's what I have:
public static func fetchItems(numberOfItems: Int, completion: ([Item]?, NSError?) -> ()) -> Void {
var items: [Item] = []
let group = dispatch_group_create()
for (var itemId = 0; itemId < numberOfItems; itemId++) {
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
APIManager.fetchItemWithId(itemId) {
(item, error) in
guard let item = item else {
// handle error
}
print("Item \(itemId) downloaded")
items.append(item)
}
}
}
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
completion(items, nil)
}
}
My output ends up:
nil
Item 0 downloaded
Item 1 downloaded
Item 2 downloaded
etc
While I am dispatching the the calls for the Item
s asynchronously, the calls themselves have another asynchronous action inside - illustrated by APIManager.fetchItemWithId
in the example. So, ultimately, my completion
is hit before the API requests resolve.
What am I missing here?
Your problem lies in this async call to APIManager
. Your block, dispatched to group finishes before block in that call. Actually, all blocks in group finishes before it. If you have option to call sync version of fetchItemWithId
- use it here. If not - use dispatch_semaphore_t
.
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
APIManager.fetchItemWithId(itemId) {
(item, error) in
guard let item = item else {
// handle error
dispatch_semaphore_signal(semaphore);
}
print("Item \(itemId) downloaded")
items.append(item)
dispatch_semaphore_signal(semaphore);
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
Feel free to ask, if something left unclear. Or, if I misunderstood your intensions
Update
I decided to add some comments to make execution flow clear why everything happens the way it does
for (var itemId = 0; itemId < numberOfItems; itemId++) {
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
//1 reach this point for "numberOfItems" times
APIManager.fetchItemWithId(itemId) {
(item, error) in
guard let item = item else {
// handle error
}
//4 we have no guarantee, when this point will be reached relatively to execution flow of "fetchItems" method.
//Actually, looks like it is dispatched to some low priority background queue.
//When it is first reached, "group" blocks have already been dispatched and successfully executed
print("Item \(itemId) downloaded")
items.append(item)
}
//2 previous block has been added to some queue. Reach this point for "numberOfItems" times
}
}
//3 reach this point. Most likely all group blocks have already been executed, so completion block is dispatched almost immediately
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
completion(items, nil)
}