Search code examples
iosmultithreadingswiftgrand-central-dispatchdispatch-async

How to get nested asynchronous calls resolved before completion block


Here's the setup. I have a method that has a completion block in which I want to return a list of Items. These Items are fetched from an API. I'd like to have each of the fetches happen asynchronously but ultimately return the Items 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 Items 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?


Solution

  • 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)
    }