Search code examples
swiftcllocationclgeocoder

CLGeocoder() returns nil unexpectedly


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?


Solution

  • 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.