Swift - 从反向地理编码生成地址格式

38
我试图使用Swift 3中的CLGeocoder生成格式化完整地址。我参考了这个SO线程以获取以下代码。但是,有时应用程序会在以下行出现'nil'错误而崩溃:
//Address dictionary
print(placeMark.addressDictionary ?? "")

问题:

  1. 我该如何连接从GeoCoder检索的这些值以形成完整的地址?(街道+城市+等等)
  2. 当该函数无法找到地址时,我该如何处理nil错误?

完整代码:

func getAddress() -> String {
        var address: String = ""

        let geoCoder = CLGeocoder()
        let location = CLLocation(latitude: selectedLat, longitude: selectedLon)
        //selectedLat and selectedLon are double values set by the app in a previous process

        geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in

            // Place details
            var placeMark: CLPlacemark!
            placeMark = placemarks?[0]

            // Address dictionary
            //print(placeMark.addressDictionary ?? "")

            // Location name
            if let locationName = placeMark.addressDictionary!["Name"] as? NSString {
                //print(locationName)
            }

            // Street address
            if let street = placeMark.addressDictionary!["Thoroughfare"] as? NSString {
                //print(street)
            }

            // City
            if let city = placeMark.addressDictionary!["City"] as? NSString {
                //print(city)
            }

            // Zip code
            if let zip = placeMark.addressDictionary!["ZIP"] as? NSString {
                //print(zip)
            }

            // Country
            if let country = placeMark.addressDictionary!["Country"] as? NSString {
                //print(country)
            }

        })

        return address;
    } 

查看我的答案在Swift 4.1 Xcode 9.4.1中。你甚至可以获得村庄名称的详细信息。https://dev59.com/XGQn5IYBdhLWcg3wroyL#51797299 - Naresh
addressDictionary 在 iOS 11 及以后版本中已被弃用 - Ashutosh Shukla
12个回答

90
func getAddressFromLatLon(pdblLatitude: String, withLongitude pdblLongitude: String) {
        var center : CLLocationCoordinate2D = CLLocationCoordinate2D()
        let lat: Double = Double("\(pdblLatitude)")!
        //21.228124
        let lon: Double = Double("\(pdblLongitude)")!
        //72.833770
        let ceo: CLGeocoder = CLGeocoder()
        center.latitude = lat
        center.longitude = lon

        let loc: CLLocation = CLLocation(latitude:center.latitude, longitude: center.longitude)


        ceo.reverseGeocodeLocation(loc, completionHandler:
            {(placemarks, error) in
                if (error != nil)
                {
                    print("reverse geodcode fail: \(error!.localizedDescription)")
                }
                let pm = placemarks! as [CLPlacemark]

                if pm.count > 0 {
                    let pm = placemarks![0]
                    print(pm.country)
                    print(pm.locality)
                    print(pm.subLocality)
                    print(pm.thoroughfare)
                    print(pm.postalCode)
                    print(pm.subThoroughfare)
                    var addressString : String = ""
                    if pm.subLocality != nil {
                        addressString = addressString + pm.subLocality! + ", "
                    }
                    if pm.thoroughfare != nil {
                        addressString = addressString + pm.thoroughfare! + ", "
                    }
                    if pm.locality != nil {
                        addressString = addressString + pm.locality! + ", "
                    }
                    if pm.country != nil {
                        addressString = addressString + pm.country! + ", "
                    }
                    if pm.postalCode != nil {
                        addressString = addressString + pm.postalCode! + " "
                    }


                    print(addressString)
              }
        })

    }

并且最后打印addressstring。你得到了你的整个地址。 - Himanshu Moradiya
1
这里有一个问题。在等待从LatLng处理地址之前,该函数会经过这个过程。因此,最终我得到了一个空字符串。 - Jay
1
@RickGrimesLikesWalkerSoup,请查看我在项目中使用的整个方法。你也可以使用它。 - Himanshu Moradiya
3
@HimanshuMoradiya:非常好用,非常感谢。但是需要注意的一点是,不应该强制解包 placemark ,因为它有时可能为空。这种情况发生在我的身上,可以使用 if let 语句来避免意外崩溃。祝愉快编程,干杯! - iPeter
如何返回地址?我无法返回。请帮忙。 - Nishant Singh
显示剩余5条评论

26

格式化地址很困难,因为每个国家都有其自己的格式。

通过几行代码,您可以获取每个国家的正确地址格式,并让苹果处理差异。

自iOS 11以来,您可以获取“联系人框架”地址:

extension CLPlacemark {
    @available(iOS 11.0, *)
    open var postalAddress: CNPostalAddress? { get }
}

这个扩展是Contacts框架的一部分。 这意味着,在你进行操作之前,你在XCode代码补全中看不到该功能。

import Contacts

有了这个额外的导入,你可以做类似这样的事情

CLGeocoder().reverseGeocodeLocation(location, preferredLocale: nil) { (clPlacemark: [CLPlacemark]?, error: Error?) in
    guard let place = clPlacemark?.first else {
        print("No placemark from Apple: \(String(describing: error))")
        return
    }

    let postalAddressFormatter = CNPostalAddressFormatter()
    postalAddressFormatter.style = .mailingAddress
    var addressString: String?
    if let postalAddress = place.postalAddress {
        addressString = postalAddressFormatter.string(from: postalAddress)
    }
}
并以地址所在国家的格式获得地址格式化的地址。
格式化程序甚至支持作为attributedString的格式化。
在iOS 11之前,您可以自己将CLPlacemark转换为CNPostalAddress,并仍然可以使用CNPostalAddressFormatter的特定于国家/地区的格式。

我喜欢这种方法,但是当我添加您的扩展时,我看到'postalAddress'在其自己的类型中使用? - GarySabo
只需删除苹果添加的扩展名,一切都应该正常。 - Gerd Castan
哦哈哈,我的错。 - GarySabo
2
救命稻草。感谢你的帮助。 - Jake
@Zonker.in.Geneva的名称和地址字典在构造函数中是可选的。您可以将它们设置为nil。 - Gerd Castan
显示剩余3条评论

8

这是我用 Swift 3 编写的代码。

func getAdressName(coords: CLLocation) {

    CLGeocoder().reverseGeocodeLocation(coords) { (placemark, error) in
            if error != nil {
                print("Hay un error")
            } else {

                let place = placemark! as [CLPlacemark]
                if place.count > 0 {
                    let place = placemark![0]
                    var adressString : String = ""
                    if place.thoroughfare != nil {
                        adressString = adressString + place.thoroughfare! + ", "
                    }
                    if place.subThoroughfare != nil {
                        adressString = adressString + place.subThoroughfare! + "\n"
                    }
                    if place.locality != nil {
                        adressString = adressString + place.locality! + " - "
                    }
                    if place.postalCode != nil {
                        adressString = adressString + place.postalCode! + "\n"
                    }
                    if place.subAdministrativeArea != nil {
                        adressString = adressString + place.subAdministrativeArea! + " - "
                    }
                    if place.country != nil {
                        adressString = adressString + place.country!
                    }

                    self.lblPlace.text = adressString
                }
            }
        }
  }

你可以轻松调用上述函数,如下所示:
let cityCoords = CLLocation(latitude: newLat, longitude: newLon)
cityData(coord: cityCoords)

不要强制解包,否则在没有网络的情况下会导致整个应用程序崩溃。让place = placemark! as [CLPlacemark]。 - Lukasz D

6
  1. 为解决地址为空的问题,你可以使用类属性来保存添加的值,或者使用闭包将值返回给调用函数
  2. 为了解决崩溃问题,需要避免强制解包可选项

使用闭包可以这样实现:

// Using closure
func getAddress(handler: @escaping (String) -> Void)
{
    var address: String = ""
    let geoCoder = CLGeocoder()
    let location = CLLocation(latitude: selectedLat, longitude: selectedLon)
    //selectedLat and selectedLon are double values set by the app in a previous process
    
    geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
        
        // Place details
        var placeMark: CLPlacemark?
        placeMark = placemarks?[0]
        
        // Address dictionary
        //print(placeMark.addressDictionary ?? "")
        
        // Location name
        if let locationName = placeMark?.addressDictionary?["Name"] as? String {
            address += locationName + ", "
        }
        
        // Street address
        if let street = placeMark?.addressDictionary?["Thoroughfare"] as? String {
            address += street + ", "
        }
        
        // City
        if let city = placeMark?.addressDictionary?["City"] as? String {
            address += city + ", "
        }
        
        // Zip code
        if let zip = placeMark?.addressDictionary?["ZIP"] as? String {
            address += zip + ", "
        }
        
        // Country
        if let country = placeMark?.addressDictionary?["Country"] as? String {
            address += country
        }
        
       // Passing address back
       handler(address)
    })
}

你可以像这样调用该方法:
getAddress { (address) in
    print(address)
}

在这一行出现了错误。 geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in“逃逸闭包捕获了非逃逸参数'handler'” - Ashu
@Ashu 在后续的 Swift 版本中,你必须为闭包写上 @escaping,已更新答案。请查看。 - Midhun MP

3
要进行字符串连接,您可以将return address替换为以下内容:
return "\(locationName), \(street), \(city), \(zip), \(country)"

这并没有提供问题的答案。如果要批评或请求作者澄清,请在他们的帖子下留言。-【来自审查】 - Mayur Prajapati
@Mack 这是关于“如何连接...”的答案 - Makaille
也许您应该添加更多关于如何操作、使用方法、在哪里使用这一行代码等方面的解释。现在只是简单地放置一行代码并不能总是得到正确的解决方案。 - Mayur Prajapati

2

保持简单 - 一个完整的Swift 3和4兼容的视图控制器示例,用于从用户位置获取格式化的地址字符串(如果您想在字符串中添加CLPlacemark中提供的其他键以获得更多信息):

import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

let manager = CLLocationManager()
let geocoder = CLGeocoder()

var locality = ""
var administrativeArea = ""
var country = ""

override func viewDidLoad() {
    super.viewDidLoad()

    manager.delegate = self
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.requestWhenInUseAuthorization()
    manager.startUpdatingLocation()

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let location = locations[0]
        manager.stopUpdatingLocation()

    geocoder.reverseGeocodeLocation(location, completionHandler: {(placemarks, error) in
        if (error != nil) {
            print("Error in reverseGeocode")
            }

        let placemark = placemarks! as [CLPlacemark]
        if placemark.count > 0 {
            let placemark = placemarks![0]
            self.locality = placemark.locality!
            self.administrativeArea = placemark.administrativeArea!
            self.country = placemark.country!
        }
    })
}

func userLocationString() -> String {
    let userLocationString = "\(locality), \(administrativeArea), \(country)"
    return userLocationString
}

}

在这个例子中调用print(userLocationString())将打印:郊区,州,国家。
不要忘记事先向Info.plist文件添加“隐私-使用时位置”描述,以允许用户授予权限给您的应用程序使用位置服务。

街道地址怎么样? - Zonker.in.Geneva
@Zonker.in.Geneva 请查看说明中的链接,特别是“thoroughfare”。在这个例子中,您将使用 placemark.subThoroughfare、placemark.thoroughfare,也许还有 placemark.subLocality 和 placemark.postalCode。https://developer.apple.com/documentation/corelocation/clplacemark/1423814-thoroughfare - elarcoiris

2
func getAddress(from coordinate: CLLocationCoordinate2D, completion: @escaping (String) -> Void) {
        let geoCoder = CLGeocoder()
        let location = CLLocation.init(latitude: coordinate.latitude, longitude: coordinate.longitude)
        
        geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
            
            // check for errors
            guard let placeMarkArr = placemarks else {
                completion("")
                debugPrint(error ?? "")
                return
            }
            // check placemark data existence
            
            guard let placemark = placeMarkArr.first, !placeMarkArr.isEmpty else {
                completion("")
                return
            }
            // create address string
            
            let outputString = [placemark.locality,
                                placemark.subLocality,
                                placemark.thoroughfare,
                                placemark.postalCode,
                                placemark.subThoroughfare,
                                placemark.country].compactMap { $0 }.joined(separator: ", ")
            
            completion(outputString)
        })
    }

2
以下是这里答案的2-3行版本:
    func getAddress(placemarks: [CLPlacemark]) -> String {
        guard let placemark = placemarks.first, !placemarks.isEmpty else {return ""}
        let outputString = [placemark.locality,
                            placemark.subLocality,
                            placemark.thoroughfare,
                            placemark.postalCode,
                            placemark.subThoroughfare,
                            placemark.country].compactMap{$0}.joined(separator: ", ")
        print(outputString)
        return outputString
    }

1
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: vehicleLocation.latitude, longitude: vehicleLocation.latitude), completionHandler: {(placemarks, error) -> Void in

  guard error == nil else {completionHandler(nil); return}

  guard let place = placemarks else {completionHandler(nil); return}

  if place.count > 0 {
    let pm = place[0]

    var addArray:[String] = []
    if let name = pm.name {
      addArray.append(name)
    }
    if let thoroughfare = pm.thoroughfare {
      addArray.append(thoroughfare)
    }
    if let subLocality = pm.subLocality {
      addArray.append(subLocality)
    }
    if let locality = pm.locality {
      addArray.append(locality)
    }
    if let subAdministrativeArea = pm.subAdministrativeArea {
      addArray.append(subAdministrativeArea)
    }
    if let administrativeArea = pm.administrativeArea {
      addArray.append(administrativeArea)
    }
    if let country = pm.country {
      addArray.append(country)
    }
    if let postalCode = pm.postalCode {
      addArray.append(postalCode)
    }

    let addressString = addArray.joined(separator: ",\n")

    print(addressString)

    completionHandler(addressString)
  }
  else { completionHandler(nil)}
})

1
func convertLatLongToAddress(latitude:Double, longitude:Double) {
    let geoCoder = CLGeocoder()
    let location = CLLocation(latitude: latitude, longitude: longitude)
    var labelText = ""
    geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in

        var placeMark: CLPlacemark!
        placeMark = placemarks?[0]

        if placeMark != nil {
            if let name = placeMark.name {
                labelText = name
            }
            if let subThoroughfare = placeMark.subThoroughfare {
                if (subThoroughfare != placeMark.name) && (labelText != subThoroughfare) {
                    labelText = (labelText != "") ? labelText + "," + subThoroughfare : subThoroughfare
                }
            }
            if let subLocality = placeMark.subLocality {
                if (subLocality != placeMark.subThoroughfare) && (labelText != subLocality) {
                    labelText = (labelText != "") ? labelText + "," + subLocality : subLocality
                }
            }
            if let street = placeMark.thoroughfare {
                if (street != placeMark.subLocality) && (labelText != street) {
                    labelText = (labelText != "") ? labelText + "," + street : street
                }
            }
            if let locality = placeMark.locality {
                if (locality != placeMark.thoroughfare) && (labelText != locality) {
                    labelText = (labelText != "") ? labelText + "," + locality : locality
                }
            }
            if let city = placeMark.subAdministrativeArea {
                if (city != placeMark.locality) && (labelText != city) {
                    labelText = (labelText != "") ? labelText + "," + city : city
                }
            }
            if let state = placeMark.postalAddress?.state {
                if (state != placeMark.subAdministrativeArea) && (labelText != state) {
                    labelText = (labelText != "") ? labelText + "," + state : state
                }

            }
            if let country = placeMark.country {
                labelText = (labelText != "") ? labelText + "," + country : country
            }
            // labelText gives you the address of the place
        }
    })
}

我在这里进行了改进,加入了地点名称。这使得地址更有意义。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接