1

I have a list of locations (about 30 elements):

var locations: [CLLocation] = [
        CLLocation(latitude: 45.471172, longitude: 9.163317),
        ...
]

My purpose is to get street names from that list, so I decided to use CLGeocoder(). I call a function inside a viewDidLoad(), and every location is processed by lookUpCurrentLocation().

override func viewDidLoad() {
    super.viewDidLoad()       

        for location in locations {
            lookUpCurrentLocation(location: location, completionHandler: { streetName in
            print(streetName)
        })
    }

}

func lookUpCurrentLocation(location: CLLocation, completionHandler: @escaping (String?) -> Void) {
    CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (placemarks, error) in
            let placemark = placemarks?[0]
            completionHandler(placemarks?[0].name)
    })
}

My problem: when the app starts, it prints a list of nil or only first two nil and the others street names.

terminal image 1

terminal image 2

I aspect to see the whole list processed without any nil. Any hints?

Edoardo
  • 657
  • 7
  • 24
  • 1
    try printing your error `print(error)` **After initiating a reverse-geocoding request, do not attempt to initiate another reverse- or forward-geocoding request. Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value CLError.Code.network to your completion handler.** – Leo Dabus Nov 17 '19 at 12:52
  • @LeoDabus Thank you, it prints me: Error Domain=kCLErrorDomain Code=2 (null) – Edoardo Nov 17 '19 at 12:58
  • Code 2 is `CLError.Code.network.rawValue`. – Rob Nov 17 '19 at 15:02

2 Answers2

4

As Leo said, you don’t want to run the requests concurrently. As the documentation says:

After initiating a reverse-geocoding request, do not attempt to initiate another reverse- or forward-geocoding request. Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value CLError.Code.network to your completion handler.

There are a few approaches to make these asynchronous requests run sequentially:

  1. The simple solution is to make the method recursive, invoking the next call in the completion handler of the prior one:

    func retrievePlacemarks(at index: Int = 0) {
        guard index < locations.count else { return }
    
        lookUpCurrentLocation(location: locations[index]) { name in
            print(name ?? "no  name found")
            DispatchQueue.main.async {
                self.retrievePlacemarks(at: index + 1)
            }
        }
    }
    

    And then, just call

    retrievePlacemarks()
    

    FWIW, I might use first rather than [0] when doing the geocoding:

    func lookUpCurrentLocation(location: CLLocation, completionHandler: @escaping (String?) -> Void) {
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, _ in
            completionHandler(placemarks?.first?.name)
        }
    }
    

    I don’t think it’s possible for reverseGeocodeLocation to return a non-nil, zero-length array (in which case your rendition would crash with an invalid subscript error), but the above does the exact same thing as yours, but also eliminates that potential error.

  2. An elegant way to make asynchronous tasks run sequentially is to wrap them in an asynchronous Operation subclass (such as a general-purpose AsynchronousOperation seen in the latter part of this answer).

    Then you can define a reverse geocode operation:

    class ReverseGeocodeOperation: AsynchronousOperation {
        private static let geocoder = CLGeocoder()
        let location: CLLocation
        private var geocodeCompletionBlock: ((String?) -> Void)?
    
        init(location: CLLocation, geocodeCompletionBlock: @escaping (String?) -> Void) {
            self.location = location
            self.geocodeCompletionBlock = geocodeCompletionBlock
        }
    
        override func main() {
            ReverseGeocodeOperation.geocoder.reverseGeocodeLocation(location) { placemarks, _ in
                self.geocodeCompletionBlock?(placemarks?.first?.name)
                self.geocodeCompletionBlock = nil
                self.finish()
            }
        }
    }
    

    Then you can create a serial operation queue and add your reverse geocode operations to that queue:

    private let geocoderQueue: OperationQueue = {
        let queue = OperationQueue()
        queue.name = Bundle.main.bundleIdentifier! + ".geocoder"
        queue.maxConcurrentOperationCount = 1
        return queue
    }()
    
    func retrievePlacemarks() {
        for location in locations {
            geocoderQueue.addOperation(ReverseGeocodeOperation(location: location) { string in
                print(string ?? "no name found")
            })
        }
    }
    
  3. If targeting iOS 13 and later, you can use Combine, e.g. define a publisher for reverse geocoding:

    extension CLGeocoder {
        func reverseGeocodeLocationPublisher(_ location: CLLocation, preferredLocale locale: Locale? = nil) -> AnyPublisher<CLPlacemark, Error> {
            Future<CLPlacemark, Error> { promise in
                self.reverseGeocodeLocation(location, preferredLocale: locale) { placemarks, error in
                    guard let placemark = placemarks?.first else {
                        return promise(.failure(error ?? CLError(.geocodeFoundNoResult)))
                    }
                    return promise(.success(placemark))
                }
            }.eraseToAnyPublisher()
        }
    }
    

    And then you can use a publisher sequence, where you specify maxPublishers of .max(1) to make sure it doesn’t perform them concurrently:

    private var placemarkStream: AnyCancellable?
    
    func retrievePlacemarks() {
        placemarkStream = Publishers.Sequence(sequence: locations).flatMap(maxPublishers: .max(1)) { location in
            self.geocoder.reverseGeocodeLocationPublisher(location)
        }.sink { completion in
            print("done")
        } receiveValue: { placemark in
            print("placemark:", placemark)
        }
    }
    

There are admittedly other approaches to make asynchronous tasks run sequentially (often involving calling wait using semaphores or dispatch groups), but I don’t think that those patterns are advisable, so I’ve excluded them from my list of alternatives, above.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

Here's an implementation using Combine, with a persistent cache. Need more intelligent cache expiry logic, etc, but it is a starting point. Patches welcome.

https://gist.github.com/lhoward/dd6b64fb8f5782c933359e0d54bcb7d3

Luke Howard
  • 469
  • 5
  • 5