8

I have a method with a completion handler

func postLandGradingImages(cellHolder: Array<ImagesData>, completionHandler:@escaping (_ result:Bool) -> Void) {

        //Define bool for returning data

        var returnedResults = false

        //Call API

        WebService().postLandGradingImages(cellHolder)
        {
            (result: Bool) in

            DispatchQueue.main.async {

                //Return our results

                returnedResults = result
                completionHandler(returnedResults)

            }

        }

    }

and I am calling this method inside a loop like so:

for asset: PHAsset in photoAssets
{
    self.postLandGradingImages(cellHolder: [ImagesData(jobNo: self.JobNo, ImageBytes: imageStr)]) { result in

    }
}

What I am trying to do is if this fails at some point, display an alert and stop looping and after the loop is done and all my calls returned true display an alert at the end.

This is what I have tried:

var returned = false

        for asset: PHAsset in photoAssets
        {
            imageManager.requestImage(for: asset, targetSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight), contentMode: .aspectFill, options: options, resultHandler: { (image, info) in

                let imageData:Data = UIImagePNGRepresentation(image!)!

                let imageStr = imageData.base64EncodedString()

                self.postLandGradingImages(cellHolder: [ImagesData(jobNo: self.JobNo, ImageBytes: imageStr)]) { result in

                    returned = result

                    if(returned == false)
                    {
                        self.customAlert(title: "Error", message: "There was an error when saving data, please try again later.")
                    }

                }

            })
        }

        if(returned == true)
        {
            self.customAlert(title: "Error", message: “All Good“)
        }

But my alert saying All Good never comes up as returned gets checked before even my first call. What am I doing wrong and how do I accomplish what I am trying to accomplish?

user979331
  • 11,039
  • 73
  • 223
  • 418

1 Answers1

16

The problem is that your for loop will complete very quickly. In fact, as you've seen, the loop will finish before even one of the completion blocks is called. This is the nature of asynchronous processing.

By using DispatchGroup you can setup your code so it will perform a block of code only after all of the completion blocks have finished, regardless of how quickly the loop itself completes.

Also note that you have two levels of async calls inside your loop.

Below is how you should setup your code. Also note I fixed several other issues such as forced-unwraps.

var returned = true // assume success for all

let group = DispatchGroup()

for asset in photoAssets {
    group.enter() // for imageManager
    imageManager.requestImage(for: asset, targetSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight), contentMode: .aspectFill, options: options, resultHandler: { (image, info) in
        if let image = image, let let imageData = UIImagePNGRepresentation(image) {
            let imageStr = imageData.base64EncodedString()

            group.enter()
            self.postLandGradingImages(cellHolder: [ImagesData(jobNo: self.JobNo, ImageBytes: imageStr)]) { result in
                if !result {
                    returned = false // we had a failure
                }
                group.leave()
            }
        }
        group.leave() // imageManager
    })
}

group.notify(queue: DispatchQueue.main) {
    if returned {
        self.customAlert(title: "Success", message: “All Good“)
    } else {
        self.customAlert(title: "Error", message: "There was an error when saving data, please try again later.")
    }
}

With all of that in place, you need to update postLandGradingImages. There's no need to use the main queue for the completion handler.

func postLandGradingImages(cellHolder: Array<ImagesData>, completionHandler:@escaping (_ result:Bool) -> Void) {
    //Call API
    WebService().postLandGradingImages(cellHolder) { (result: Bool) in
        //Return our results
        completionHandler(result)
    }
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • I've actually missed the get image call :) But you can easily enter the group only once :D – Mihai Fratu May 08 '18 at 16:15
  • @MihaiFratu No, you can't enter the group only once. – rmaddy May 08 '18 at 16:16
  • Of course you can :P You enter for every iteration of the loop and leave it when either `postLandGradingImages` finishes or when the image loading fails. But that was supposed to be just a small remark ;) – Mihai Fratu May 08 '18 at 16:19
  • @MihaiFratu Yeah, I supposed that would work. But I prefer one `enter/leave` pair per async call. It's more balanced. – rmaddy May 08 '18 at 16:24
  • There is no means of stopping the loop as soon as something goes wrong as requested in the question. I'm not sure that is really an issue though. – JeremyP May 08 '18 at 16:25
  • @JeremyP See the first paragraph of my answer. The loop is done before you can even detect the first failure. – rmaddy May 08 '18 at 16:27