CLGeocoder() 意外返回 nil

Posted

技术标签:

【中文标题】CLGeocoder() 意外返回 nil【英文标题】:CLGeocoder() returns nil unexpectedly 【发布时间】:2019-11-17 12:08:07 【问题描述】:

我有一个位置列表(大约 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 的情况下得到处理。 有什么提示吗?

【问题讨论】:

尝试打印您的错误print(error) 在发起反向地理编码请求后,不要尝试发起另一个反向或正向地理编码请求。每个应用程序的地理编码请求都有速率限制,因此在短时间内发出过多请求可能会导致某些请求失败。当超过最大速率时,地理编码器会将值为 CLError.Code.network 的错误对象传递给您的完成处理程序。 @LeoDabus 谢谢,它打印了我:Error Domain=kCLErrorDomain Code=2 (null) 代码 2 是CLError.Code.network.rawValue 【参考方案1】:

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

发起反向地理编码请求后,请勿尝试发起另一个反向或正向地理编码请求。每个应用程序的地理编码请求都有速率限制,因此在短时间内发出过多请求可能会导致某些请求失败。当超过最大速率时,地理编码器会将值为 CLError.Code.network 的错误对象传递给您的完成处理程序。

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

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

    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 不可能返回一个非nil 的零长度数组(在这种情况下,您的再现会因无效的下标错误而崩溃),但上面的内容完全相同像你一样,但也消除了潜在的错误。

    使异步任务按顺序运行的一种优雅方法是将它们包装在异步Operation 子类中(例如在latter part of this answer 中看到的通用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")
            )
        
    
    

    如果针对 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),但我认为这些模式并不可取,因此我已将它们排除在我的列表之外替代品,上面。

【讨论】:

【参考方案2】:

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

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

【讨论】:

谢谢!写这篇文章我脑筋疼,但它实际上只是上面 Rob 方法的一个实现。 我刚刚更新了它,结果你想缓存地标本身而不是 Just 发布者。 我还添加了一个惯用的 PlacemarkReader 视图,您可以使用它来包装另一个视图,类似于 GeometryReader。

以上是关于CLGeocoder() 意外返回 nil的主要内容,如果未能解决你的问题,请参考以下文章

CLGeocoder 在其他国家/地区的返回位置

CLGeocoder 曾经返回一个地标

CLGeocoder 只返回第一个查找结果

Swift 中的 CLGeocoder - 使用 reverseGeocodeLocation 时无法返回字符串

CLGeocoder 在 NSString 但不在 NSLog 中返回 (null)

将 MKMapView 更新为从 CLGeocoder 返回的 CLPlacemark