Apple provides a semiphor to handle multiple API calls running async called DispatchGroup.

struct MockAPI {
    static func networkRequestWithDelay(seconds: Double, completion: @escaping (JSON) -> ()) {
        DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
            completion(["foo": "bar"])
        }
    }
}

With this MockAPI we can simulate multiple slow network calls. We want to wait until all three are finished before taking an action (like updating the UI). You could nest them:

MockAPI.networkRequestWithDelay(seconds: 1) { (json1) in
    MockAPI.networkRequestWithDelay(seconds: 2) { (json2) in
        MockAPI.networkRequestWithDelay(seconds: 5) { (json) in
            updateDisplay()
        }
    }
}

But then you’re only running one-at-a-time. The total time here will be 8 seconds.

With a DispatchGroup they all start at the same time, and as their completion closures fire they notify the DispatchGroup they’re finished. When all three finish the DispatchGroup runs its notify closure.

override func viewDidLoad() {
    let group = DispatchGroup()
    
    group.enter()
    MockAPI.networkRequestWithDelay(seconds: 1) { (json) in
        group.leave()
    }
    
    group.enter()
    MockAPI.networkRequestWithDelay(seconds: 2) { (json) in
        group.leave()
    }
    
    group.enter()
    MockAPI.networkRequestWithDelay(seconds: 5) { (json) in
        group.leave()
    }
    
    group.notify(queue: .global(qos: .default)) {
        self.updateDisplay()
    }
}

private func updateDisplay() {
    DispatchQueue.main.async {
        // bounce back to the main thread
        // set labels, images, etc.
    }
}

Download Sample Project on GitHub