7

I am trying to use grand central dispatch to wait for files to finish download before continuing. This question is a spin-off from this one: Swift (iOS), waiting for all images to finish downloading before returning.

I am simply trying to find out how to get dispatch_group_wait (or similar) to actually wait and not just continue before the downloads have finished. Note that if I use NSThread.sleepForTimeInterval instead of calling downloadImage, it waits just fine.

What am I missing?

class ImageDownloader {

    var updateResult = AdUpdateResult()

    private let fileManager = NSFileManager.defaultManager()
    private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.adDirectory, isDirectory: true)

    private let group = dispatch_group_create()
    private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)

    func downloadImages(imageFilesOnServer: [AdFileInfo]) {

        dispatch_group_async(group, downloadQueue) {

            for serverFile in imageFilesOnServer {
                print("Start downloading \(serverFile.fileName)")
                //NSThread.sleepForTimeInterval(3) // Using a sleep instead of calling downloadImage makes the dispatch_group_wait below work
                self.downloadImage(serverFile)
            }
        }
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // This does not wait for downloads to finish.  Why?

        print("All Done!") // It gets here too early!
    }

    private func downloadImage(serverFile: AdFileInfo) {

        let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)

        Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
        .response { _, _, _, error in
            if let error = error {
                print("Error downloading \(serverFile.fileName): \(error)")
            } else {
                self.updateResult.filesDownloaded++
                print("Done downloading \(serverFile.fileName)")
            }
        }
    }
} 

Note: these downloads are in response to an HTTP POST request and I am using an HTTP server (Swifter) which does not support asynchronous operations, so I do need to wait for the full downloads to complete before returning a response (see original question referenced above for more details).

Community
  • 1
  • 1
Jim Balo
  • 639
  • 6
  • 22

4 Answers4

11

When using dispatch_group_async to call methods that are, themselves, asynchronous, the group will finish as soon as all of the asynchronous tasks have started, but will not wait for them to finish. Instead, you can manually call dispatch_group_enter before you make the asynchronous call, and then call dispatch_group_leave when the asynchronous call finish. Then dispatch_group_wait will now behave as expected.

To accomplish this, though, first change downloadImage to include completion handler parameter:

private func downloadImage(serverFile: AdFileInfo, completionHandler: (NSError?)->()) {
    let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)

    Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
        .response { _, _, _, error in
            if let error = error {
                print("Error downloading \(serverFile.fileName): \(error)")
            } else {
                print("Done downloading \(serverFile.fileName)")
            }
            completionHandler(error)
    }
}

I've made that a completion handler that passes back the error code. Tweak that as you see fit, but hopefully it illustrates the idea.

But, having provided the completion handler, now, when you do the downloads, you can create a group, "enter" the group before you initiate each download, "leave" the group when the completion handler is called asynchronously.

But dispatch_group_wait can deadlock if you're not careful, can block the UI if done from the main thread, etc. Better, you can use dispatch_group_notify to achieve the desired behavior.

func downloadImages(_ imageFilesOnServer: [AdFileInfo], completionHandler: @escaping (Int) -> ()) {
    let group = DispatchGroup()

    var downloaded = 0

    group.notify(queue: .main) {
        completionHandler(downloaded)
    }

    for serverFile in imageFilesOnServer {
        group.enter()

        print("Start downloading \(serverFile.fileName)")

        downloadImage(serverFile) { error in
            defer { group.leave() }

            if error == nil {
                downloaded += 1
            }
        }
    }
}

And you'd call it like so:

downloadImages(arrayOfAdFileInfo) { downloaded in
    // initiate whatever you want when the downloads are done

    print("All Done! \(downloaded) downloaded successfully.")
}

// but don't do anything contingent upon the downloading of the images here

For Swift 2 and Alamofire 3 answer, see previous revision of this answer.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Rob, I have a similar issue here: http://stackoverflow.com/questions/37201735/how-to-make-inner-async-request-complete-first-before-completing-outer-async-req do you think you can assist? – Pangu May 14 '16 at 08:39
  • Great answer, this helped me out a lot doing something similar with Firebase. – kelsheikh Aug 23 '16 at 14:58
  • The point of **`dispatch_group_wait` can deadlock if you're not careful** saved me! – fujianjin6471 Oct 21 '16 at 14:29
7

In Swift 3...

let dispatchGroup = DispatchGroup()

dispatchGroup.enter()
// do something, including background threads

dispatchGroup.leave()

dispatchGroup.notify(queue: DispatchQueue.main) {
    // completion code
}

https://developer.apple.com/reference/dispatch/dispatchgroup

CodeOverRide
  • 4,431
  • 43
  • 36
1

The code is doing exactly what you are telling it to.

The call to dispatch_group_wait will block until the block inside the call to dispatch_group_async is finished.

The block inside the call to dispatch_group_async will be finished when the for loop completes. This will complete almost immediately since the bulk of the work being done inside the downloadImage function is being done asynchronously.

This means the for loop finishes very quickly and that block is done (and dispatch_group_wait stops waiting) long before any of the actual downloads are completed.

I would make use of dispatch_group_enter and dispatch_group_leave instead of dispatch_group_async.

I would change your code to something like the following (not tested, could be typos):

class ImageDownloader {

    var updateResult = AdUpdateResult()

    private let fileManager = NSFileManager.defaultManager()
    private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.adDirectory, isDirectory: true)

    private let group = dispatch_group_create()
    private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)

    func downloadImages(imageFilesOnServer: [AdFileInfo]) {

        dispatch_async(downloadQueue) {
            for serverFile in imageFilesOnServer {
                print("Start downloading \(serverFile.fileName)")
                //NSThread.sleepForTimeInterval(3) // Using a sleep instead of calling downloadImage makes the dispatch_group_wait below work
                self.downloadImage(serverFile)
            }
        }

        dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // This does not wait for downloads to finish.  Why?

        print("All Done!") // It gets here too early!
    }

    private func downloadImage(serverFile: AdFileInfo) {
        dispatch_group_enter(group);

        let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)

        Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
        .response { _, _, _, error in
            if let error = error {
                print("Error downloading \(serverFile.fileName): \(error)")
            } else {
                self.updateResult.filesDownloaded++
                print("Done downloading \(serverFile.fileName)")
            }
            dispatch_group_leave(group);
        }
    }
} 

This change should do what you need. Each call to downloadImage enters the group and it doesn't leave the group until the download completion handler is called.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
1

Using this pattern, the final line will execute when the other tasks are finished.

let group = dispatch_group_create()

dispatch_group_enter(group)
// do something, including background threads
dispatch_group_leave(group) // can be called on a background thread

dispatch_group_enter(group)
// so something
dispatch_group_leave(group)

dispatch_group_notify(group, mainQueue) {
    // completion code
}
Mundi
  • 79,884
  • 17
  • 117
  • 140