using dispatch group in multi for loop with urlsession tasks

Viewed 860

I have using a dispatch group wait() that block my a for loop from completing the code until a set of urlsession tasks (in another loop with completion handler) to be completed before appending new element to my array

the current code will finish the first loop before the second loop of urlClass.selectfoodURL is completed

I want to append the array in meal history after my urlfood for loop is completed on of the problem in my approach of using dispatch groups is the wait(), when my select food is called the urlsession stuck and doesn’t complete with group.wait

func userSnackHistoryArray() {
    let group = DispatchGroup()
    let Arrays // array of dictionary
    for array in Arrays {

        var generateMeal = MealDetails() // struct type
        do {
            let aa = try JSONDecoder().decode(userSnack.self, from: array)
            generateMeal.names = convertToJsonFile.type

            for name in generateMeal.names!{
                group.enter()

                urlClass.selectfoodURL(foodName: name){ success in
                    generateMeal.units!.append(allVariables.selectedUnit)
                    group.leave()
                }
            }
            // my select food is called but the urlsession stuck and doesnt complete with group.wait is active
            // group.wait()
            mealHistory.append(generateMeal)
        } catch { }
    }

    group.notify(queue: .main){
        print("complete")
    }
}

I have shortened my code to focus on the problem ,, I can split my code into two functions and solve the problem , but I want to use only one function any suggestions or ideas ?

1 Answers

Rather than waiting, you should just create a local array of values to be added, and then add them when it’s done:

func retrieveSnacks() {
    var snacksToAdd: [Snack] = []

    let group = DispatchGroup()

    ...

    for url in urls {
        group.enter()
        fetchSnack(with: url) { result in
            dispatchPrecondition(condition: .onQueue(.main))   // note, I’m assuming that this closure is running on the main queue; if not, dispatch this appending of snacks (and `leave` call) to the main queue

            if case .success(let snack) = result {
                snacksToAdd.append(snack)
            }

            group.leave()
        }
    }

    // when all the `leave` calls are called, only then append the results

    group.notify(queue: .main) {
        self.snacks += snacksToAdd

        // trigger UI update, or whatever, here
    }
}

Note, the above does not assure that the objects are added in the original order. If you need that, you can use a dictionary to build the temporary results and then append the results in sorted order:

func retrieveSnacks() {
    var snacksToAdd: [URL: Snack] = [:]

    let group = DispatchGroup()

    ...

    for url in urls {
        group.enter()
        fetchSnack(with: url) { result in
            if case .success(let snack) = result {
                snacksToAdd[url] = snack
            }
            group.leave()
        }
    }

    group.notify(queue: .main) {
        let sortedSnacks = urls.compactMap { snacksToAdd[$0] }
        self.snacks += sortedSnacks

        // trigger UI update, or whatever, here
    }
}

Finally, I might suggest adopting a completion handler pattern:

func retrieveSnacks(completion: @escaping ([Snack]) -> Void) {
    var snacksToAdd: [URL: Snack] = [:]

    let group = DispatchGroup()

    ...

    for url in urls {
        group.enter()
        fetchSnack(with: url) { result in
            if case .success(let snack) = result {
                snacksToAdd[url] = snack
            }
            group.leave()
        }
    }

    group.notify(queue: .main) {
        let sortedSnacks = urls.compactMap { snacksToAdd[$0] }
        completion(sortedSnacks)
    }
}

retrieveSnacks { addedSnacks in
    self.snacks += addedSnacks
    // update UI here
}

This pattern ensures that you don’t entangle your network-related code with your UI code.


I apologize that the above is somewhat refactored from your code snippet, but there wasn’t enough there for me to illustrate what precisely it would look like. But hopefully the above illustrates the pattern and you can see how you’d apply it to your code base. So, don’t get lost in the details, but focus on the basic pattern of building records to be added in a local variable and only update the final results in the .notify block.

FWIW, this is the method signature for the method that the above snippets are using to asynchronously fetch the objects in question.

func fetchSnack(with url: URL, completion: @escaping (Result<Snack, Error>) -> Void) {
    ...

    // if async fetch not successful 

    DispatchQueue.main.async {
        completion(.failure(error))
    }

    // if successful

    DispatchQueue.main.async {
        completion(.success(snack))
    }
}
Related