CLGeocoder() returns 意外地为零

CLGeocoder() returns nil unexpectedly

我有一个位置列表(大约 30 个元素):

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

我的目的是从该列表中获取街道名称,因此我决定使用 CLGeocoder()。 我在 viewDidLoad() 中调用一个函数,每个位置都由 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)
    })
}

我的问题: 当应用程序启动时,它会打印一个 nil 或仅前两个 nil 和其他街道名称的列表。

terminal image 1

terminal image 2

我查看整个列表的处理过程,没有任何 nil。 有什么提示吗?

正如 Leo 所说,您不想 运行 并发请求。正如 the documentation 所说:

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.

有几种方法可以按顺序 运行 发出这些异步请求:

  1. 简单的解决方案是使方法递归,在前一个完成处理程序中调用下一个调用:

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

    然后,只需调用

    retrievePlacemarks()
    

    FWIW,在进行地理编码时我可能会使用 first 而不是 [0]

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

    我认为 reverseGeocodeLocation 到 return 非 nil 零长度数组是不可能的(在这种情况下,您的再现会因无效下标错误而崩溃),但上面的内容与你的完全相同,但也消除了潜在的错误。

  2. 顺序执行异步任务 运行 的一种优雅方法是将它们包装在异步 Operation 子类中(例如在中看到的通用 AsynchronousOperation ).

    然后你可以定义一个反向地理编码操作:

    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()
            }
        }
    }
    

    然后您可以创建一个串行操作队列并将您的反向地理编码操作添加到该队列:

    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. 如果定位 iOS 13 及更高版本,您可以使用 Combine,例如为反向地理编码定义发布者:

    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()
        }
    }
    

    然后您可以使用发布者序列,在其中指定 maxPublishers.max(1) 以确保它不会同时执行它们:

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

诚然还有其他方法可以按顺序执行异步任务 运行(通常涉及使用信号量或调度组调用 wait),但我认为这些模式不可取,所以我已将它们排除在我上面的备选方案列表之外。

这是一个使用 Combine 和持久缓存的实现。需要更智能的缓存过期逻辑等,但这是一个起点。欢迎补丁。

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