Wait until swift for loop with asynchronous network requests finishes executing

Viewed 111893

I would like a for in loop to send off a bunch of network requests to firebase, then pass the data to a new view controller once the the method finishes executing. Here is my code:

var datesArray = [String: AnyObject]()

for key in locationsArray {       
    let ref = Firebase(url: "http://myfirebase.com/" + "\(key.0)")
    ref.observeSingleEventOfType(.Value, withBlock: { snapshot in

        datesArray["\(key.0)"] = snapshot.value
    })
}
// Segue to new view controller here and pass datesArray once it is complete 

I have a couple concerns. First, how do I wait until the for loop is finished and all the network requests are complete? I can't modify the observeSingleEventOfType function, it is part of the firebase SDK. Also, will I create some sort of race condition by trying to access the datesArray from different iterations of the for loop (hope that makes sense)? I've been reading about GCD and NSOperation but I'm a bit lost as this is the first app I've built.

Note: Locations array is an array containing the keys I need to access in firebase. Also, it's important that the network requests are fired off asynchronously. I just want to wait until ALL the asynchronous requests complete before I pass the datesArray to the next view controller.

10 Answers

Swift 3 or 4

If you don't care about orders, use @paulvs's answer, it works perfectly.

else just in case if anyone wants to get the result in order instead of fire them concurrently, here is the code.

let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "any-label-name")
let dispatchSemaphore = DispatchSemaphore(value: 0)

dispatchQueue.async {

    // use array categories as an example.
    for c in self.categories {

        if let id = c.categoryId {

            dispatchGroup.enter()

            self.downloadProductsByCategory(categoryId: id) { success, data in

                if success, let products = data {

                    self.products.append(products)
                }

                dispatchSemaphore.signal()
                dispatchGroup.leave()
            }

            dispatchSemaphore.wait()
        }
    }
}

dispatchGroup.notify(queue: dispatchQueue) {

    DispatchQueue.main.async {

        self.refreshOrderTable { _ in

            self.productCollectionView.reloadData()
        }
    }
}

Details

  • Xcode 10.2.1 (10E1001), Swift 5

Solution

import Foundation

class SimultaneousOperationsQueue {
    typealias CompleteClosure = ()->()

    private let dispatchQueue: DispatchQueue
    private lazy var tasksCompletionQueue = DispatchQueue.main
    private let semaphore: DispatchSemaphore
    var whenCompleteAll: (()->())?
    private lazy var numberOfPendingActionsSemaphore = DispatchSemaphore(value: 1)
    private lazy var _numberOfPendingActions = 0

    var numberOfPendingTasks: Int {
        get {
            numberOfPendingActionsSemaphore.wait()
            defer { numberOfPendingActionsSemaphore.signal() }
            return _numberOfPendingActions
        }
        set(value) {
            numberOfPendingActionsSemaphore.wait()
            defer { numberOfPendingActionsSemaphore.signal() }
            _numberOfPendingActions = value
        }
    }

    init(numberOfSimultaneousActions: Int, dispatchQueueLabel: String) {
        dispatchQueue = DispatchQueue(label: dispatchQueueLabel)
        semaphore = DispatchSemaphore(value: numberOfSimultaneousActions)
    }

    func run(closure: ((@escaping CompleteClosure) -> Void)?) {
        numberOfPendingTasks += 1
        dispatchQueue.async { [weak self] in
            guard   let self = self,
                    let closure = closure else { return }
            self.semaphore.wait()
            closure {
                defer { self.semaphore.signal() }
                self.numberOfPendingTasks -= 1
                if self.numberOfPendingTasks == 0, let closure = self.whenCompleteAll {
                    self.tasksCompletionQueue.async { closure() }
                }
            }
        }
    }

    func run(closure: (() -> Void)?) {
        numberOfPendingTasks += 1
        dispatchQueue.async { [weak self] in
            guard   let self = self,
                    let closure = closure else { return }
            self.semaphore.wait(); defer { self.semaphore.signal() }
            closure()
            self.numberOfPendingTasks -= 1
            if self.numberOfPendingTasks == 0, let closure = self.whenCompleteAll {
                self.tasksCompletionQueue.async { closure() }
            }
        }
    }
}

Usage

let queue = SimultaneousOperationsQueue(numberOfSimultaneousActions: 1, dispatchQueueLabel: "AnyString")
queue.whenCompleteAll = { print("All Done") }

 // add task with sync/async code
queue.run { completeClosure in
    // your code here...

    // Make signal that this closure finished
    completeClosure()
}

 // add task only with sync code
queue.run {
    // your code here...
}

Full sample

import UIKit

class ViewController: UIViewController {

    private lazy var queue = { SimultaneousOperationsQueue(numberOfSimultaneousActions: 1,
                                                           dispatchQueueLabel: "AnyString") }()
    private weak var button: UIButton!
    private weak var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(frame: CGRect(x: 50, y: 80, width: 100, height: 100))
        button.setTitleColor(.blue, for: .normal)
        button.titleLabel?.numberOfLines = 0
        view.addSubview(button)
        self.button = button

        let label = UILabel(frame: CGRect(x: 180, y: 50, width: 100, height: 100))
        label.text = ""
        label.numberOfLines = 0
        label.textAlignment = .natural
        view.addSubview(label)
        self.label = label

        queue.whenCompleteAll = { [weak self] in self?.label.text = "All tasks completed" }

        //sample1()
        sample2()
    }

    func sample1() {
        button.setTitle("Run 2 task", for: .normal)
        button.addTarget(self, action: #selector(sample1Action), for: .touchUpInside)
    }

    func sample2() {
        button.setTitle("Run 10 tasks", for: .normal)
        button.addTarget(self, action: #selector(sample2Action), for: .touchUpInside)
    }

    private func add2Tasks() {
        queue.run { completeTask in
            DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds(1)) {
                DispatchQueue.main.async { [weak self] in
                    guard let self = self else { return }
                    self.label.text = "pending tasks \(self.queue.numberOfPendingTasks)"
                }
                completeTask()
            }
        }
        queue.run {
            sleep(1)
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                self.label.text = "pending tasks \(self.queue.numberOfPendingTasks)"
            }
        }
    }

    @objc func sample1Action() {
        label.text = "pending tasks \(queue.numberOfPendingTasks)"
        add2Tasks()
    }

    @objc func sample2Action() {
        label.text = "pending tasks \(queue.numberOfPendingTasks)"
        for _ in 0..<5 { add2Tasks() }
    }
}

Update for iOS 15+ (Swift 5.5)

I've added a more modern solution for Swift 5.5 and iOS 15+ because this toolchain includes major URLSession API improvements, that are not specific to Firebase or Alamofire. The code uses async / await i.e. Structured Concurrency. It's what Apple recommends for concurrent requests on the latest iOS versions (iOS 13.0+).

We now achieve the same result as DispatchGroups with fewer lines of code and more customisation. This answer will help users who used to queue URLSession requests and wait for these to complete.

Task group example code

The right tool is a TaskGroup if we have a dynamic number of requests (variable-sized array).

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
  var thumbnails: [String: UIImage] = [:]
  try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
    for id in ids {
      group.addTask {
        return (id, try await fetchOneThumbnail(withID: id))
      }
    }
    for try await (id, thumbnail) in group {
      thumbnails[id] = thumbnail
    }
  }
  return thumbnails
}

This also uses the for await loop (AsyncSequence) to wait for tasks to complete. for try await is an example of a throwing AsyncSequence. The throwing syntax is because the new asynchronous URLSession.data(for:) family of methods are throwing functions.

async let example code

async let syntax works for a fixed number of requests.

let reqOne = urlRequest(for: keyOne) // Function that returns a unique URLRequest object for this key. i.e. different URLs or format.
async let (dataOne, _) = URLSession.shared.data(for: reqOne)

let reqTwo = urlRequest(for: keyTwo)
async let (dataTwo, _) = URLSession.shared.data(for: reqTwo)

guard let parsedData = parseInformation(from: try? await dataOne) else {
    // Call function to parse image, text or content from data.
      continue 
}
// Act on parsed data if needed.

guard let parsedDataTwo = parseInformation(from: try? await dataTwo) else {
    // Call function to parse image, text or content from data.
      continue 
}
// Act on the second requests parsed data if needed.

// Here, we know that the queued requests have all completed.

The syntax where I don't await for the request to finish immediately is called async let.

This code example could be adapted with variable-sized arrays but isn't recommended by Apple. This is because async let doesn't always allow the requests to be processed as soon as they arrive.

The benefits of this approach are cleaner code that's easier to write, safer, and avoiding deadlocks/threading issues.

Note

The exact syntax of TaskGroup and async let may change in the future. Currently, Structured Concurrency has improved a lot during its early releases and is now stable for production.

Apple has clarified that the underlying mechanics of grouped and asynchronous tasks are mostly finalised (approved in Swift Evolution). An example of some syntax changes already includes the replacement of async { with Task {.

We can do this with recursion. Get idea from below code :

var count = 0

func uploadImages(){

    if count < viewModel.uploadImageModelArray.count {
        let item = viewModel.uploadImageModelArray[count]
        self.viewModel.uploadImageExpense(filePath: item.imagePath, docType: "image/png", fileName: item.fileName ?? "", title: item.imageName ?? "", notes: item.notes ?? "", location: item.location ?? "") { (status) in

            if status ?? false {
                // successfully uploaded
            }else{
                // failed
            }
            self.count += 1
            self.uploadImages()
        }
    }
}

In the original question, it was contemplated how to know when all of the queries were done, returning the results in a dictionary. paulvs has answered (+1) that question: If using old completion handler closure pattern, use a dispatch group to know when they’re done. And, nowadays, if using Swift concurrency, use the pattern proposed by Pranav Kasetti.

But, if you need the results in an ordered array, though, one should not make the requests, themselves, run sequentially. You pay a serious performance penalty (often more than 3× slower) if you do that. And if you achieve this through the use semaphores, you introduce all sorts of other inefficiencies and deadlock risks.

Instead, if you really need the results in an ordered array, you should employ paulvs’s answer to populate a dictionary from the concurrent requests, but then build an array of the results at the end. E.g.,

let array = ids.compactMap { resultsDictionary[$0] }

For example, Swift concurrency handles this all very gracefully:


func fetch(for ids: [Id]) async throws -> [Foo] {
    try await withThrowingTaskGroup(of: (Id, Foo).self) { [self] group in
        for id in ids {
            group.addTask { (id, try await fetch(for: id)) }
        }
        
        let dictionary = try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
        return ids.compactMap { dictionary[$0] }
    }
}

But even if you are using the older completion handler closure pattern the idea is the same: Store your results in a dictionary, enjoy concurrency, and build the sorted array at the end if you really need it.

Related