I have a function that takes a [CLLocation]
as an input. Inside a while
loop it splits it into chunks, for each chunk makes a MKDirection
request, store the response in a new [CLLocation]
and returns it once completed.
The problem is that all the chunks in the new array are not sequential, so the resulting route jumps all over the place. Ho do I wait for the previous request to get a response before making a new one? I tried DispatchQueue.global().sync
and DispatchQueue.main.sync
but it doesn't make a difference.
I tried to implement first answer from Cannot wait for the result of MKDirections.calculate, getting nil instead of it that seemed my same problem, but I'm not understanding how to adapt it to my case.
Can you please help me to get the responses sequentially?
This is the function, the commented out part is for the lates bit of the route, and that will be the the last request.
As always many thanks for your help and time.
func repositionLocation2(route: [CLLocation], completion: @escaping ([CLLocation]) -> Void) {
let group = DispatchGroup()
var pos = 0
var nextPos = 3
var repositioned = [CLLocation]()
// repositioned.append(route.first!)
guard route.count > nextPos else {print("Reposision Location failed, not enough positions");return}
let request = MKDirections.Request()
request.requestsAlternateRoutes = false
request.transportType = .walking
while pos < route.count - nextPos {
print(" pos in \(pos)")
// repositioned.removeAll()
group.enter()
// get a small chunk of the input route
let a = route[pos].coordinate//repositioned.last!.coordinate//
let b = route[pos + nextPos].coordinate
// get directions for the small chunk
request.source = MKMapItem(placemark: MKPlacemark(coordinate: a))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: b))
let directions = MKDirections(request: request)
// DispatchQueue.main.sync {
// DispatchQueue.global().sync {
// group.enter()
directions.calculate { [unowned self] response, error in
if let err = error {
print("direction error : \(err)")
}
guard let unwrappedResponse = response else {print("no suggested routes available"); return }
print("Response is: \(unwrappedResponse.debugDescription)")
guard let coord = unwrappedResponse.routes.first?.steps else {print("No coordinates");return}
print("coord is: \(coord)")
// save response coordinates into a new array
for location in coord {
let point: CLLocation = CLLocation(latitude: location.polyline.coordinate.latitude, longitude: location.polyline.coordinate.longitude)
print("point is: \(point)") // prints a correct CLLocation with coordinates
repositioned.append(point)
print("repositioned in for loop is : \(repositioned)") // prints just first appended location CLLocation with coordinates
// group.leave()
}
// group.wait() // hangs the app
completion(repositioned)
}
// }
print("repositioned in while loop is : \(repositioned)")
// shift to nex addiacent chunk
pos += 3
nextPos += 3
}
// // last chunk
// let a = route[pos - 5].coordinate//repositioned.last!.coordinate
// let b = route.last?.coordinate
// request.source = MKMapItem(placemark: MKPlacemark(coordinate: a))
// request.destination = MKMapItem(placemark: MKPlacemark(coordinate: b!))
// let directions = MKDirections(request: request)
// directions.calculate { [unowned self] response, error in
// if let err = error {
// print("direction error : \(err)")
// }
// guard let unwrappedResponse = response else {print("no suggested routes available"); return }
// print("Response is: \(unwrappedResponse.debugDescription)")
// guard let coord = unwrappedResponse.routes.first?.steps else {print("No coordinates");return}
// print("coord is: \(coord)")
// for location in coord {
//
// let point: CLLocation = CLLocation(latitude: location.polyline.coordinate.latitude, longitude: location.polyline.coordinate.longitude)
// print("point is: \(point)")
// repositioned.append(point)
// print("repositioned in for loop is : \(repositioned)")
// }
// completion(repositioned)
// }
// print("repositioned in while loop is : \(repositioned)")
}
When you have a series of asynchronous tasks (which may finish in any arbitrary order) where you want the results in order, just save it into a structure for which order doesn’t matter, just sorting it at the end. E.g., you could use a dictionary indexed by the integer index:
var routes: [Int: [CLLocationCoordinate2D]] = [:]
Then when any given loop finishes, it can just update this dictionary:
routes[i] = ...
And if you want a sorted flat array at the end:
let coordinates = steps.sorted { $0.0 < $1.0 }
.flatMap { $0.1 }
Or, you might use a pre-populated array of optionals, where you can insert the particular route at the correct position in the array:
var routes: [[CLLocationCoordinate2D]?] = Array(repeating: nil, count: pointCount - 1)
And when you want to update one:
routes[i-1] = ...
And then, at the end, you can remove optionals with compactMap
and flatten it with flatMap
:
let coordinates = steps.compactMap { $0 }.flatMap { $0 }
Thus:
func fetchDirections(_ locations: [CLLocation], completion: @escaping ([CLLocationCoordinate2D]) -> Void) {
let pointCount = locations.count
guard pointCount > 1 else { return }
var routes: [[CLLocationCoordinate2D]?] = Array(repeating: nil, count: pointCount - 1)
let group = DispatchGroup()
for i in 1 ..< pointCount {
group.enter()
directions(from: locations[i-1], to: locations[i]).calculate { response, error in
defer { group.leave() }
guard
error == nil,
let response = response,
let route = response.routes.first
else { return }
routes[i-1] = self.coordinates(for: route.steps)
}
}
group.notify(queue: .main) {
let coordinates = routes.compactMap { $0 }.flatMap { $0 }
completion(coordinates)
}
}
func directions(from: CLLocation, to: CLLocation) -> MKDirections {
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: from.coordinate))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: to.coordinate))
request.requestsAlternateRoutes = false
request.transportType = .walking
return MKDirections(request: request)
}
func coordinates(for steps: [MKRoute.Step]) -> [CLLocationCoordinate2D] {
guard !steps.isEmpty else { return [] }
var coordinates: [CLLocationCoordinate2D] = []
for step in steps {
let count = step.polyline.pointCount
let pointer = step.polyline.points()
for i in 0 ..< count {
let coordinate = pointer[i].coordinate
if coordinate.latitude != coordinates.last?.latitude, coordinate.longitude != coordinates.last?.longitude {
coordinates.append(coordinate)
}
}
}
return coordinates
}
Where:
fetchDirections(locations) { coordinates in
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
self.mapView.addOverlay(polyline)
}
Yielding, for a stroll through Apple’s complex:
By the way, notice that I’m not just using the coordinate
of the polyline
of the MKRoute.Step
. That’s the center of the polyline. You presumably want to iterate through the points()
.
That having been said, when I get directions, it’s generally just to show it on the map, so I generally just add the polyline
as an overlay, directly, and don’t bother decomposing it into arrays of CLLocationCoordinate2D
, but I assume you have some other reason for wanting to do that.