24

I want to format CLPlacemark to string.

The well known way is to use ABCreateStringWithAddressDictionary but it was deprecated in iOS 9. Warning tells me to use CNPostalAddressFormatter instead.

However, CNPostalAddressFormatter can only format CNPostalAddress. There is no way to properly convert CLPlacemark to CNPostalAddress; only these 3 properties are shared by CLPlacemark and CNPostalAddress: country, ISOcountryCode, and postalCode.

So how should I format CLPlacemark to string now?

shim
  • 9,289
  • 12
  • 69
  • 108
Mostafa
  • 1,522
  • 2
  • 18
  • 34

4 Answers4

27

Take the placemark's addressDictionary and use its "FormattedAddressLines" key to extract the address string. Note that this is an array of the lines of the string.

(You are correct, however, that the Apple developers tasked with converting to the Contacts framework seem to have forgotten completely about the interchange between Address Book and CLPlacemark. This is a serious bug in the Contacts framework - one of many.)


EDIT Since I posted that answer originally, Apple fixed this bug. A CLPlacemark now has a postalAddress property which is a CNPostalAddress, and you can then use a CNPostalAddressFormatter to get a nice multi-line address string. Be sure to import Contacts!

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    Note to readers: in the 2+ years since this correct answer was given, `addressDictionary` has been depreciated (in iOS 11). The juggernaut of technology steamrolls on... – AmitaiB Feb 21 '18 at 20:56
  • ...and `CLPlacemark` got `var postalAddress: CNPostalAddress? { get }` property, sherlocking the whole thread! – AmitaiB Oct 08 '18 at 21:32
  • @AmitaiB Yes, it's purely of historical interest now...! – matt Oct 08 '18 at 21:53
15

Swift 3.0

if let lines = myCLPlacemark.addressDictionary?["FormattedAddressLines"] as? [String] {
    let placeString = lines.joined(separator: ", ")
    // Do your thing
}
Max Desiatov
  • 5,087
  • 3
  • 48
  • 56
guido
  • 2,792
  • 1
  • 21
  • 40
  • FormattedAddressLines return an array with this order: Name, Street, City, State, ZIP, Country. if you print placeString, you see something like this: "Apple Inc., 1 Infinite Loop, Cupertino, CA 95014, United States". – Marlon Ruiz May 02 '17 at 20:11
8

Swift 4.1 (and 3 & 4, save 1 line)

I read the question to ask 'How might I implement this?':

extension String {
    init?(placemark: CLPlacemark?) {
        // Yadda, yadda, yadda
    }
}

Two Methods

I first went for porting the AddressDictionary method, as did other posters. But that means losing the power and flexibility of the CNPostalAddress class and formatter. Hence, method 2.

extension String {
    // original method (edited)
    init?(depreciated placemark1: CLPlacemark?) {
    // UPDATE: **addressDictionary depreciated in iOS 11**
        guard
            let myAddressDictionary = placemark1?.addressDictionary,
            let myAddressLines = myAddressDictionary["FormattedAddressLines"] as? [String]
    else { return nil }

        self.init(myAddressLines.joined(separator: " "))
}

    // my preferred method - let CNPostalAddressFormatter do the heavy lifting
    init?(betterMethod placemark2: CLPlacemark?) {
        // where the magic is:
        guard let postalAddress = CNMutablePostalAddress(placemark: placemark2) else { return nil }
        self.init(CNPostalAddressFormatter().string(from: postalAddress))
    }
}

Wait, what is that CLPlacemarkCNPostalAddress initializer??

extension CNMutablePostalAddress {
    convenience init(placemark: CLPlacemark) {
        self.init()
        street = [placemark.subThoroughfare, placemark.thoroughfare]
            .compactMap { $0 }           // remove nils, so that...
            .joined(separator: " ")      // ...only if both != nil, add a space.
    /*
    // Equivalent street assignment, w/o flatMap + joined:
        if let subThoroughfare = placemark.subThoroughfare,
            let thoroughfare = placemark.thoroughfare {
            street = "\(subThoroughfare) \(thoroughfare)"
        } else {
            street = (placemark.subThoroughfare ?? "") + (placemark.thoroughfare ?? "")
        } 
    */
        city = placemark.locality ?? ""
        state = placemark.administrativeArea ?? ""
        postalCode = placemark.postalCode ?? ""
        country = placemark.country ?? ""
        isoCountryCode = placemark.isoCountryCode ?? ""
        if #available(iOS 10.3, *) {
            subLocality = placemark.subLocality ?? ""
            subAdministrativeArea = placemark.subAdministrativeArea ?? ""
        }
    }
}

Usage

func quickAndDirtyDemo() {
    let location = CLLocation(latitude: 38.8977, longitude: -77.0365)

    CLGeocoder().reverseGeocodeLocation(location) { (placemarks, _) in
        if let address = String(depreciated: placemarks?.first) {
            print("\nAddress Dictionary method:\n\(address)") }

        if let address = String(betterMethod: placemarks?.first) {
            print("\nEnumerated init method:\n\(address)") }
    }
}

/* Output:
Address Dictionary method:
The White House 1600 Pennsylvania Ave NW Washington, DC  20500 United States

Enumerated init method:
1600 Pennsylvania Ave NW
Washington DC 20500
United States
*/

Whoever read until here gets a free T-shirt. (not really)

*This code works in Swift 3 & 4, except that flatMap for removing nil values has been depreciated/renamed to compactMap in Swift 4.1 (Doc here, or see SE-187 for the rationale).

AmitaiB
  • 1,656
  • 1
  • 19
  • 19
  • You could significantly improve your code by 1. not using other libraries in sample code 2. using `flatMap` + `joined` 3. null coalescing since all members of `CNPostalAddress` are non null. ps: Then you wouldn't have to promise free t-shirt :P – pronebird Aug 20 '17 at 15:16
  • 1
    @Andy 1] Thanks, hidden extension removed. 2] `flatMap` + `joined`. Hmm. I'll add an alternative in comments, tell me if you like it better. 3] It's *because* of that fact that they can't be assigned `CLPlacemark`'s String Optionals -- CLPlacemark's properties need to be coalesced, not `CNPostalAddress`'s. – AmitaiB Feb 21 '18 at 20:50
  • 1
    For what it's worth, in iOS 11 `CLPlacemark` has a `CNPostalAddress` getter. – Mr Rogers Oct 08 '18 at 15:32
2

Swift 3.0 Helper Method

class func addressFromPlacemark(_ placemark:CLPlacemark)->String{
        var address = ""

        if let name = placemark.addressDictionary?["Name"] as? String {
            address = constructAddressString(address, newString: name)
        }

        if let city = placemark.addressDictionary?["City"] as? String {
            address = constructAddressString(address, newString: city)
        }

        if let state = placemark.addressDictionary?["State"] as? String {
            address = constructAddressString(address, newString: state)
        }

        if let country = placemark.country{
          address = constructAddressString(address, newString: country)
        }

        return address
      }
Sourabh Sharma
  • 8,222
  • 5
  • 68
  • 78