0

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?

Chris
  • 1,828
  • 6
  • 40
  • 108

2 Answers2

1

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)
    }
}
Jawad Ali
  • 13,556
  • 3
  • 32
  • 49
  • thanks for the answer but It is not loading at all now. Not showing any error either so I guess its stuck somewhere – Chris Jul 18 '20 at 14:16
  • you can debug it ... whats happening ... let me review the code again... – Jawad Ali Jul 18 '20 at 14:31
  • Never used `Semaphore` before so Im not quite sure how to debug it – Chris Jul 18 '20 at 14:32
  • plus my debug skills are reduced to `print` Statements :D – Chris Jul 18 '20 at 14:32
  • `let dispatchSemaphore = DispatchSemaphore(value: 1)` put 1 here instead of 0 – Jawad Ali Jul 18 '20 at 14:33
  • still just loading – Chris Jul 18 '20 at 14:35
  • oh .. you put leave in else as well =) remove it from else case .. as its already in defer condition – Jawad Ali Jul 18 '20 at 14:36
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/218098/discussion-between-chris-and-jawadali). – Chris Jul 18 '20 at 14:37
  • hey man :D There is one more error case where your function is not working.. If the user logs in for the first time so there is only one list with no wishes. Then it is in an endless load. Any idea how to fix that? – Chris Jul 20 '20 at 17:04
  • I will look into this case – Jawad Ali Jul 20 '20 at 19:55
  • I fixed it ! working without any problems .. for now :D – Chris Jul 25 '20 at 11:41
  • hey :D never ending story.. I fixed the problem with an empty list but that brings um another problem: It fire two completions which looks weird on the actual device as the data reloads twice. Any chance you could have another look at it? – Chris Jul 28 '20 at 13:04
0

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
}}
Omer Faruk Ozturk
  • 1,722
  • 13
  • 25