Swift - Async calls in loop

Hi guys,

I hope you are doing fine.
I am trying to achieve following thing:
  1. ) Fetch data array from database (async call)

  2. ) Iterate over fetched data array

  3. ) Fetch additional information about each object (async call)

  4. ) Create a new data array with all the information and return it back

Currently, I have following approach


Code Block  
self.dataAccessService.fetchRepliesByCommentId(completionHandler: { (commentReplyArray) in
      for var i in 0..<commentReplyArray.count {
        let commentReply = commentReplyArray[i]
        let commentItem = CommentItem()
         
        self.fetchDetailsAboutCommentReply(commentReplyObject: commentReply) { (commentItem) in
          commentItem.commentObject = commentReply
           
          dataSource.insert(commentItem, at: index + i + 1) -> APP CRASHES HERE, i is never 0 here
          ips.append(IndexPath(row: index + i + 1 , section: 0))
                                                          
          if (i == commentReplyArray.count - 1) {
            self.delegate?.didLoadReplies(dataSource: dataSource, ips: ips)
          }
        }
      }
    }, commentId: commentItem.commentObject.id)

My fetchDetailsAboutCommentReply function:
Code Block
private func fetchDetailsAboutCommentReply(commentReplyObject:CommentReply, completionHandler:@escaping(CommentItem)->()) {
     let group = DispatchGroup()
     let commentItem = CommentItem()
     
     group.enter()
      self.dataAccessService.fetchUserById(completionHandler: { (userObject) in
        commentItem.userObject = userObject
        group.leave()
     }, uid: commentReplyObject.userId)
            
     group.enter()
      self.dataAccessService.fetchDownloadURLOfProfileImage(organizerId: commentReplyObject.userId) { (contentURL) in
      commentItem.userObject.contentURL = contentURL
      group.leave()
     }
     
    group.notify(queue: .main) {
      completionHandler(commentItem)
    }
  }

My question is how, I can change my code, so the loop basically "pauses" until I fetch every detail information of the iterated object, add it into the dataSource Array and then continues with the next one?

Thanks and stay healthy!

Replies

the loop basically "pauses" until I fetch every detail information of the iterated object, add it into the dataSource Array and then continues with the next one

Do you really need the "pauses"?

If your dataAccessService accepts multiple API calls simultaneously, you can send multiple requests simultaneously and construct the result when the last data is received.

My question is how, I can change my code, so the loop basically "pauses" until I fetch every detail information of the iterated object, add it into the dataSource Array and then continues with the next one?

Why do you want this ? For display purpose ?
Is it really a good idea to try to sync something which is essentially async ?

But if you really need.
loop: do you mean this loop ?
Code Block
     for var i in 0..<commentReplyArray.count {

you could try to have a global
var onGoing : Bool
set it to false line 6 before self.fetchDetailsAboutCommentReply
then wait until reset to true, line 18 (with some timeout to avoid a deadlock)
and set to true after groupNotify.

But I do not find it a very good design (it may also lead to a very slow user interface).
Hi @OOPer, I don't need a "pause" specifically. I just don't know how to construct the result with each response of the sent requests.
Hi @Claude31,

well, you can tell me if it is a good idea :) I am open for every good solution. Just to give you more background information.
To a given comment object, I load all the replies. Those replies are stored in the commentReplyArray array.
For each comment reply, I have to execute the async tasks to fetch the information, put it to result array and return it.

Yes, the replies should be displayed in a table view to a given comment. I want to add additional cells below the cell with the original comment.
Maybe I can share the whole function header as well:

Code Block  
func loadReplies(commentItem: CommentItem, isSearchBarFocused:Bool, filteredComments:[CommentItem], dataSource:[CommentItem]) {
    var dataSource = dataSource
    // Get index of the comment
    let index:Int
    if (isSearchBarFocused) {
      index = filteredComments.firstIndex(of: commentItem)!
    }
    else {
      index = dataSource.firstIndex(of: commentItem)!
    }
     
    var ips: [IndexPath] = []
             
    var results: [Int: CommentItem] = [:]
     
    self.dataAccessService.fetchRepliesByCommentId(completionHandler: { (commentReplyArray) in
      let group = DispatchGroup()
      for var i in 0..<commentReplyArray.count {
        let commentReplyObject = commentReplyArray[i]
        let commentItem = CommentItem()
         
        group.enter()
          self.dataAccessService.fetchUserById(completionHandler: { (userObject) in
            commentItem.userObject = userObject
            results[i] = commentItem
            defer { group.leave() }
         }, uid: commentReplyObject.userId)
                
         group.enter()
          self.dataAccessService.fetchDownloadURLOfProfileImage(organizerId: commentReplyObject.userId) { (contentURL) in
          commentItem.userObject.contentURL = contentURL
           defer { group.leave() }
         }
         
        group.notify(queue: .main) {
          commentItem.commentObject = commentReplyObject
          let array = commentReplyArray.compactMap { _ in results[i] }
          print(array.count)
        }
        
      }
    }, commentId: commentItem.commentObject.id)
  }

This time, I tried to move the  let group = DispatchGroup() into the loop

I just don't know how to construct the result with each response of the sent requests.

Thanks for clarification. Then using DispatchGroup may be a good choice.

Two more things to clarify:
  • Does your dataAccessService calls completionHandler always in the main thread?

(To make your code thread-safe, this is a big issue.)
  • What happens in case of communication error in fetchUserById?

(To use DispatchGroup properly, you cannot ignore error handling.)
Hi @OOPer, thanks for the reply.

My fetchUserById method looks like this
Code Block  
public func fetchUserById(completionHandler:@escaping(_ user: User)->(), uid:String?) { // throws{
    var userObject = User()
    let _userId = UserUtil.validateUserId(userId: uid)
    USER_COLLECTION?.whereField("uid", isEqualTo: _userId).getDocuments(completion: { (querySnapshot, error) in
       
      if error != nil {
        print(error?.localizedDescription as Any)
      } else {
        for document in querySnapshot!.documents {
          userObject = User(snapShot: document)
          completionHandler(userObject)
        }
      }
    })
  }


My fetchUserById method looks like this

Thanks for showing your code.

That resolves the second question I have shown.

What happens in case of communication error in fetchUserById?

Seems your fetchUserById just shows some debug message and never calls completion handler on error.
That's a very bad design when working with DispatchGroup.

Can you change the method as to call completion handler in case of error?


And another question is not answered.

Does the method getDocuments(completion:) calls completion in the main thread?
Hi OOPer (my hero in this forum :) )

Does the method getDocuments(completion:) calls completion in the main thread?

well, let me explain it like this:

  1. I trigger the loadReplies.. function once I fetched the initial comment objects from the database. The comment objects are stored in the dataSource which is passed as a parameter.

  2. Once loadReplies is called, my view controller doesn't care actually about the replies as long as they are not fully fetched and returned back via delegate pattern.

From my understanding, the fetches above are running NOT in the main thread or :)

From my understanding

I'm afraid your understanding is not enough to explain the behavior of your code.

Whether the completion handler is called in the main thread depends on the inner most async method getDocument.
Have you defined the method by yourself? Or is it provided by some third party framework?
If the latter, you need to check the documentation of the framework.
Hi OOPer,


Have you defined the method by yourself? Or is it provided by some third party framework?

It is indeed defined by a third-party library. I am using the FireBaseSDK to fetch data from the database.

 I am using the FireBaseSDK to fetch data from the database.

What does the doc of the SDK say?