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.
I aspect to see the whole list processed without any nil. Any hints?
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:
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.
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")
})
}
}
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.