Nazar Medeiros
Nazar Medeiros

Reputation: 445

Swift - Async calls in loop

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

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:

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!

Upvotes: 0

Views: 257

Answers (1)

Rob
Rob

Reputation: 437552

It is exceedingly hard to be specific because we do not have information about your data source logic, the types, etc. But, then again, I do not think we want to get into that here, anyway.

So, some general observations:

  • You should use DispatchGroup in the loop. E.g.,

    let group = DispatchGroup()
    for i in ... {
        group.enter()
        someAsyncMethod { completion in
            defer { group.leave() }
            ...
        }
    }
    group.notify(queue: .main) {
        ...
    }
    
  • As you can see, I have removed that if (i == commentReplyArray.count - 1) { ... } test because you want these to run in parallel and just because the “last” one finished doesn't mean that they've all finished. Use the DispatchGroup and its notify method to know when they're all done.

  • I am suspicious about that + 1 logic in your dataSource.insert call (we live in a zero-based-index world). E.g. the first item you insert should have an index of 0, not 1. (And if you are doing that + 1 logic because you have some extra cell in your tableview/collection view, I would suggest not entangling that offset index logic inside this routine, but let your “data source” take care of that.)

  • That probably doesn't matter because you really want to refactor this data source, anyway, so it doesn't matter the order that the fetchDetailsAboutComent completion handlers are called. E.g., build a local dictionary, and when done, build your sorted array and pass that back:

    // dictionary for results, so order doesn't matter
    
    var results: [Int: CommentReply] = [:]  // I don't know what the type of your comment/reply is, so adjust this as needed
    
    let group = DispatchGroup()
    for i in 0..<20 {
        group.enter()
        someAsyncMethod { completion in
            defer { group.leave() }
            ...
            results[i] = ...
        }
    }
    
    group.notify(queue: .main) {
        // now build array from the dictionary
    
        let array = (0..<20).compactMap { results[i] }
    
        dataSource?.insert(array)
        ...
    }
    

    If you really want to call the data source as results come in, you can theoretically do that, but you want to make sure that you're not just inserting into an array but rather that the dataSource object can handle the results as they come in, out of order.

    You suggest that you want the loop to “pause” for one request until the prior one finishes, and I would strenuously advise against that pattern, as it will make the process far slower (basically compounding network latency effects). You really want logic that can let the requests run in parallel.

Upvotes: 1

Related Questions