Completion Handler in For/In Loop - Swift

Swift - completion handler in loop

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

Swift Completion Handler For Loop to be performed once instead of 10 times due to the loop

Use a DispatchGroup. You can enter the dispatch group each time you call the async code and then leave each time it's done. Then when everything is finished it will call the notify block and you can call your handler. Here's a quick example of what that would look like:

let dispatchGroup = DispatchGroup()
let array = []

for i in array {
dispatchGroup.enter()
somethingAsync() {
dispatchGroup.leave()
}
}

dispatchGroup.notify(queue: .main) {
handler()
}

Swift - How to handle completion block in for-loop?

You can add a completionHandler to your function which you can call when the animation is completed:

func makeAnimation(value: Int, onCompletion: @escaping (LOTAnimationView) -> Void) {
let anotherAnimationView = LOTAnimationView(name: gifNames[value])
animationView = anotherAnimationView
animationView?.play(completion: { finished in
print("completed")
onCompletion(animationView)
})
}

And to use it use the following:

makeAnimation(value: 1000000) { (animationView) in
self.view.addSubview(animationView!)
}

Swift- Waiting for asynchronous for-in loop to complete before calling completion handler swift

A couple of thoughts:

  1. Avoid ever calling wait from the main thread. The use cases for that are pretty limited. The notify is a much safer way to achieve the same thing.

  2. Make sure you call leave from every path inside you’re loop. This can be achieved nicely with defer block.

So:

func foo(completion: @escaping (Double?) -> Void) {
ref.observeSingleEvent(of: .value) { snapshot in
guard let bills = snapshot.value as? [String: AnyObject] else {
//error
completion(nil)
return
}

let group = DispatchGroup()
var runningTotal = 0.0

for billId in bills.keys {
group.enter()
print("Entering")
Database.database().reference().child("bills").child(billId).observeSingleEvent(of: .value) { snapshot in
defer { group.leave() }
guard let bill = snapshot.value as? [String: AnyObject] else {
return
}
if let amount = bill["amount"] as? Double {
runningTotal += amount
}
print("Leaving")
}
}
group.notify(queue: .main) {
completion(runningTotal)
}
}
}

Swift 4 completion handler for loop not working

You need a DispatchGroup for multiple asunchounous tasks finish completion

func loopImages(qa: QAClass, assets: [DKAsset], completion: ([PhotoClass]) -> Void) {

var imageCellHolder = [PhotoClass]()

let dispa = DispatchGroup()

for x in assets
{
dispa.enter()

x.fetchOriginalImage(completeBlock: { (image, info) in

let compressedImage = image?.resizeWithWidth(width: 800)

let imageData:Data = compressedImage!.jpegData(compressionQuality: 0.5)!

imageCellHolder.append(PhotoClass(job: String(self.selectedCommunity! + self.selectedLot!), photo: imageData, itemId: qa.itemId))

dispa.leave()

})
}

dispa.notify(queue: .main) {
completion(imageCellHolder)
}

}

Using completion handlers inside while loop

You can try the following:

func getHub(triesLeft: Int = 3, completion: @escaping (Bool, Error?) -> Void) {
let timeoutInSeconds = 1.0

print("starting")
getHubCallAPI(completion: { status, error in
if error == nil {
print(status)
if status != "No documents found" {
print("Hub found")
return completion(true, nil)
}
} else {
print("error")
return completion(false, error) // comment out if the loop should continue on error
}
if triesLeft <= 1 {
print("Tried too many times")
return completion(false, nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + timeoutInSeconds) {
getHub(triesLeft: triesLeft - 1, completion: completion)
}
})
}

And just call it once like this:

getHub(triesLeft: 2, completion: { ... })

Note that unless you need it for some other reason, there is no need to return (Bool, Error?). And the second parameter is always nil - you may want to propagate your error. You could in theory return (String?, Error?).



Related Topics



Leave a reply



Submit