You say:
Suppose you have an array, and you want to iterate over each element in the array and call a function ... which accepts that element as a parameter.
The basic GCD pattern to know when a series of asynchronous tasks are done is the dispatch group:
let group = DispatchGroup()
for item in array {
    group.enter()
    someAsynchronousMethod { result in
        // do something with `result`
        group.leave()
    }
}
group.notify(queue: .main) {
    // what to do when everything is done
}
// note, don't use the results here, because the above all runs asynchronously; 
// return your results in the above `notify` block (e.g. perhaps an escaping closure).
If you wanted to constrain this to, say, a max concurrency of 4, you could use the non-zero semaphore pattern (but make sure you don't do this from the main thread), e.g.
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 4)
DispatchQueue.global().async {
    for item in array {
        group.enter()
        semaphore.wait()
    
        someAsynchronousMethod { result in
            // do something with `result`
    
            semaphore.signal()
            group.leave()
        }
    }
    
    group.notify(queue: .main) {
        // what to do when everything is done
    }
}
An equivalent way to achieve the above is with a custom asynchronous Operation subclass (using the base AsynchronousOperation class defined here or here), e.g.
class BarOperation: AsynchronousOperation {
    private var item: Bar
    private var completion: ((Baz) -> Void)?
    init(item: Bar, completion: @escaping (Baz) -> Void) {
        self.item = item
        self.completion = completion
    }
    override func main() {
        asynchronousProcess(bar) { baz in
            self.completion?(baz)
            self.completion = nil
            self.finish()
        }
    }
    func asynchronousProcess(_ bar: Bar, completion: @escaping (Baz) -> Void) { ... }
}
Then you can do things like:
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4
let completionOperation = BlockOperation {
    // do something with all the results you gathered
}
for item in array {
    let operation = BarOperation(item: item) { baz in
        // do something with result
    }
    operation.addDependency(completionOperation)
    queue.addOperation(operation)
}
OperationQueue.main.addOperation(completion)
And with both the non-zero semaphore approach and this operation queue approach, you can set the degree of concurrency to whatever you want (e.g. 1 = serial).
But there are other patterns, too. E.g. Combine offers ways to achieve this, too https://stackoverflow.com/a/66628970/1271826. Or with the new async/await introduced in iOS 15, macOS 12, you can take advantage of the new cooperative thread pools to constrain the degree of concurrency.
There are tons of different patterns.