Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift, dispatch_group_wait not waiting

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).

like image 887
Jim Balo Avatar asked Jan 25 '16 00:01

Jim Balo


2 Answers

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.

like image 96
Rob Avatar answered Oct 27 '22 00:10

Rob


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

like image 32
CodeOverRide Avatar answered Oct 27 '22 01:10

CodeOverRide