0

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!

4
  • 2
    Google “DispatchGroup”. Commented Dec 19, 2020 at 12:50
  • Hi @matt , I used DispatchGroup already. See my second code snippet Commented Dec 19, 2020 at 16:52
  • 1
    But you didn’t use it for the loop. Commented Dec 19, 2020 at 17:02
  • Minor point, but I would advise against for var i in ... syntax unless i is really mutable, which it isn't. Commented Dec 19, 2020 at 17:43

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

4 Comments

Do you find that defer { group.leave() } actually works? Seems like a great use of defer but in my experience it’s unreliable.
Yes, I always use defer { group.leave() } and it's rock-solid in my experience. I prefer this pattern because (a) it puts the leave visually closer to the enter, making it easier for me to reason about the code; and (b) it gets me out of tracking every little early-exit (e.g. guard, if, and switch statements) to make sure there wasn't a path of execution that I overlooked. The only case where you can't use this simple defer pattern for leave is those cases where the subsequent code is initiating yet another asynchronous call.
Hi @Rob. Well, basically, I provide the whole data source with the comments and to one given comment, I want to load all the replies. This could be one reply or multiple ones. Let's say, there are three replies in that commentReplyArray. How, do I know that I got all the information of all three replies? Once group.notify(queue: .main) is called, I might only have one reply or?
You’ve got two dispatch group types, one in fetchDetailsAboutCommentReply (which makes sure that you don’t call the its completion handler until these inner two asynchronous requests finish) and one in the loop where you call fetchDetailsAboutCommentReply repeatedly, so that outer notify won’t be invoked until all of the requests finish.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.