There are multiple ways to achieve this. Since you can make multiple requests at the same time and you need to wait for them I suggest you to create all of them simultaneously. When all return you should then return your result. There are many tools to achieve this but the most direct way is to simply create a counter. You start it with number of expected requests and then decrease it for every returned requests. Once the number turns to zero all are done. A simple example would be:
var count: Int = 10
for _ in 0..<count {
DispatchQueue.main.asyncAfter(deadline: .now() + .random(in: 1..<10)) {
count -= 1
if count == 0 {
print("All done here")
}
}
}
For your case you may want to restructure a bit. I think you should first geocode addresses to get all the placemarks. Then use array of placemarks to create pairs to use directions. Doing so will not duplicate for geocoding: Assume you are having 3 addresses A, B, C. Then to do pairs directly your would do distance(geocode(A), geocode(B)) + distance(geocode(B), geocode(C)) and you are geocoding B twice. So to avoid it rather do distance([A, B, C].map { geocode($0) }).
From your example this is what I got:
private static func generatePlacemarksFromAddresses(_ addresses: [String], completion: @escaping ((_ placemarks: [(address: String, placemark: MKPlacemark)]?, _ error: Error?) -> Void)) {
guard addresses.count > 0 else {
completion([], nil)
return
}
var requestsOut: Int = addresses.count
var placemarks: [String: MKPlacemark?] = [String: MKPlacemark?]()
addresses.forEach { address in
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { foundPlacemarks, error in
let placemark: MKPlacemark? = {
guard let location = foundPlacemarks?.first?.location else { return nil }
return MKPlacemark(coordinate: location.coordinate)
}()
placemarks[address] = placemark
requestsOut -= 1
if requestsOut == 0 {
// All are finished
// Compose ordered array or error
let erroredAddresses: [String] = placemarks.filter { $0.value == nil }.map { $0.key }
if erroredAddresses.count > 0 {
completion(nil, NSError(domain: "GEOCODING", code: 400, userInfo: ["dev_message": "Not all adresses could be geocoded. Failed with \(erroredAddresses.count) addressess: \(erroredAddresses.joined(separator: " & "))"]))
} else {
completion(addresses.map { ($0, placemarks[$0]!!) }, nil)
}
}
}
}
}
private static func calculateDirectionDistanceFromPlacemarks(_ placemarks: [(address: String, placemark: MKPlacemark)], completion: @escaping ((_ distance: Double?, _ error: Error?) -> Void)) {
guard placemarks.count > 1 else {
completion(0, nil)
return
}
var requestsOut: Int = placemarks.count-1
var overallDistance: Double = 0.0
var erroredConnections: [String] = [String]()
for index in 0..<placemarks.count-1 {
let directions = MKDirections(request: {
let request: MKDirections.Request = MKDirections.Request()
request.source = MKMapItem(placemark: placemarks[index].placemark)
request.destination = MKMapItem(placemark: placemarks[index+1].placemark)
request.transportType = MKDirectionsTransportType.automobile
return request
}())
directions.calculate(completionHandler: { (response: MKDirections.Response?, error: Error?) in
if let distance = response?.routes.first?.distance {
overallDistance += distance
} else {
erroredConnections.append(placemarks[index].address + " -> " + placemarks[index+1].address)
}
requestsOut -= 1
if requestsOut == 0 {
// All are done
if erroredConnections.count > 0 {
completion(nil, NSError(domain: "GEOCODING", code: 400, userInfo: ["dev_message": "Not all connections returned a route. Failed with \(erroredConnections.count) connections: \(erroredConnections.joined(separator: " & "))"]))
} else {
completion(overallDistance, nil)
}
}
})
}
}
And usage is pretty simple:
generatePlacemarksFromAddresses(["Ljubljana", "Canada", "1 Pall Mall East, London SW1Y 5AU", "Buckingham Palace, London SW1A 1AA", "1 Pall Mall East, London SW1Y 5AU"]) { placemarks, error in
guard let placemarks = placemarks else {
print("Placemarks could not be generated. Got error: \(error)")
return
}
calculateDirectionDistanceFromPlacemarks(placemarks) { distance, error in
if let distance = distance {
print("Got distance: \(distance)")
}
if let error = error {
print("Got error: \(error)")
}
}
}
Note that a given example produces an error because there is no root across Atlantic which is a valid result. To test with non-error example you can remove "Canada" and try with ["Ljubljana", "1 Pall Mall East, London SW1Y 5AU", "Buckingham Palace, London SW1A 1AA", "1 Pall Mall East, London SW1Y 5AU"].