Execute for loop with optional asynchronous calls in order

297 Views Asked by At

I have a function to retrieve some data which I am doing with a for-loop. Inside that it is possible that an asynchronous function is called but not necessarily. I am also firing a completion after the loop is finished which I am doing with DispatchGroup. The problem is that I need the loop to execute in order!

This is my code:

// dispatch group to make sure completion only fires when for loop is finished
let group = DispatchGroup()
// append every Wish to array at wishIDX
for document in querySnapshot!.documents {
    group.enter()
    let documentData = document.data()
    let imageUrlString = document["imageUrl"] as? String ?? ""
    let wishIDX = documentData["wishlistIDX"] as? Int ?? 0
    
    let imageView = UIImageView()
    imageView.image = UIImage()
    if let imageUrl = URL(string: imageUrlString) {
        let resource = ImageResource(downloadURL: imageUrl)
        imageView.kf.setImage(with: resource) { (result) in
            switch result {
            case .success(_):
                print("success")
                dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(image: imageView.image!)
                group.leave()
            case .failure(_):
                dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(image: UIImage())
                print("fail")
                group.leave()
            }
        }
    } else {
        dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(image: imageView.image!)
        group.leave()
    }
}
// for loop is finished -> fire completion
group.notify(queue: DispatchQueue.main) {
    completion(true, dataSourceArrayWithWishes)
}

I saw this question which is quite similar but I am struggling to apply this to my case because in my case there is the possibility that I dont make asynchronous call if there is no image. Can anyone help me out here?

2

There are 2 best solutions below

12
On BEST ANSWER

You need to use DispatchSemaphore to execute them in order

 //MARK: getWishes
static func getWishes(dataSourceArray: [Wishlist], completion: @escaping (_ success: Bool, _ dataArray: [Wishlist]) -> Void){
    
    var dataSourceArrayWithWishes = dataSourceArray
    
    let db = Firestore.firestore()
    let userID = Auth.auth().currentUser!.uid
    let group = DispatchGroup()
    let dispatchSemaphore = DispatchSemaphore(value: 0)
    for list in dataSourceArray {
        group.enter()
        db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").order(by: "wishCounter").getDocuments() { ( querySnapshot, error) in
            
            defer {
                
                print("leaving scope:\(String(describing: querySnapshot?.count))")
                group.leave()
                
                
            }
            
            if let error = error {
                print(error.localizedDescription)
                completion(false, dataSourceArrayWithWishes)
            } else {
                // dispatch group to make sure completion only fires when for loop is finished
              
                // append every Wish to array at wishIDX
                let dispatchQueue = DispatchQueue(label: "taskQueue")
                dispatchQueue.async {
                for document in querySnapshot!.documents {
                       group.enter()
                      
                    let documentData = document.data()
                    let name = documentData["name"] as? String ?? ""
                    let link = documentData["link"] as? String ?? ""
                    let price = documentData["price"] as? String ?? ""
                    let note = documentData["note"] as? String ?? ""
                    let imageUrlString = document["imageUrl"] as? String ?? ""
                    let wishIDX = documentData["wishlistIDX"] as? Int ?? 0
                    
                   
                    if let imageUrl = URL(string: imageUrlString) {
                       
                    
                          KingfisherManager.shared.retrieveImage(with: imageUrl, options: nil, progressBlock: nil, completionHandler: { result in
                            
                           var image = UIImage()
                           
                           switch result {
                           case .success(let abc):
                               image = abc.image
                               
                           case .failure(let error):
                               print(error)
                               break
                           }
                           
                        dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(name: name, link: link, price: price, note: note, image: image, checkedStatus: false))
                           
                        
                             print("Signal for next one")
                            
                            dispatchSemaphore.signal()
                            group.leave()
                            
                        })
                        print("wait for next one")
                        dispatchSemaphore.wait()
                    } else {
                        dataSourceArrayWithWishes[wishIDX].wishes.append(Wish(name: name, link: link, price: price, note: note, image: nil, checkedStatus: false))
                    }
                }
                }
                // for loop is finished -> fire completion
                
            }
        }
    }
    
    group.notify(queue: DispatchQueue.main) {
        print("notify")
        completion(true, dataSourceArrayWithWishes)
    }
}
0
On

Not directly a solution but give you an idea how to handle responses of asynchronous calls to set images in ordered. I added comments for the steps.

// filter querySnapshot!.documents. only include the ones that contain an imageUrl
let imagesUrls: [String] = []

// use DispatchGroup to handle completion
let imageDownloadGroup = DispatchGroup()

// use serial DispatchQueue to avoid data race while setting image to data model.
let imageSetQueue = DispatchQueue(label: "com.wishList.imagequeue")

// have a list with a count of valid image urls. images can be either valid or nil
var wishList: [UIImage?] = Array(repeating: nil, count: imagesUrls.count)

// use enumerated list to set image in order.
imagesUrls.enumerated().forEach { (index, item) in
    // enter to group
    imageDownloadGroup.enter()
    
    // a function to download image
    downloadImageFunction { (image) in
        
        imageSetQueue.async {
            // set image
            wishList[index] = image
            
            // leave from group
            imageDownloadGroup.leave()
        }
    }
}

imageDownloadGroup.notify(queue: .main) {
    // handle wishList and call completion
}}