How to get sequential MKDirection requests responses Swift

846 Views Asked by At

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)")

    }
1

There are 1 best solutions below

7
On BEST ANSWER

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:

enter image description here


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.