Search code examples
swiftgrand-central-dispatchdispatchdispatch-async

Nested async calls in Swift


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.


Solution

  • 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:

    • All tasks will be started nearly at the same time and run concurrently
    • Reporting the final result of all tasks via the completion handler is not shown here. Its implementation will require proper synchronisation.

    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.