-2

I'm trying to create a function that takes a postalCode and passes back the city, state, and country. The function finds the results and they can be printed from the calling closure; however, when I save the data outside they closure, they disappear.

Here's my code:

func getAddress(forPostalCode postalCode: String, completion: @escaping (_ city: String?, _ state: String?, _ country: String?, _ error: Error?) -> Void) {
    let geocoder = CLGeocoder()
    let addressString = "\(postalCode), USA"
    geocoder.geocodeAddressString(addressString) { placemarks, error in
        if let error = error {
            completion(nil, nil, nil, error)
            return
        }
        guard let placemark = placemarks?.first else {
            completion(nil, nil, nil, NSError(domain: "com.example.app", code: 1, userInfo: [NSLocalizedDescriptionKey: "No placemarks found"]))
            return
        }
        guard let city = placemark.locality else {
            completion(nil, nil, nil, NSError(domain: "com.example.app", code: 2, userInfo: [NSLocalizedDescriptionKey: "City not found"]))
            return
        }
        guard let state = placemark.administrativeArea else {
            completion(nil, nil, nil, NSError(domain: "com.example.app", code: 3, userInfo: [NSLocalizedDescriptionKey: "State not found"]))
            return
        }
        guard let country = placemark.country else {
            completion(nil, nil, nil, NSError(domain: "com.example.app", code: 4, userInfo: [NSLocalizedDescriptionKey: "Country not found"]))
            return
        }
        completion(city, state, country, nil)
    }
}

let postalCode = "10001"
var aCity: String = ""
var aState: String = ""
var aCountry: String = ""

getAddress(forPostalCode: postalCode) { city, state, country, error in
    if let error = error {
        print("Error: \(error.localizedDescription)")
        return
    }
    if let city = city, let state = state, let country = country {
        aCity = city
        aState = state
        aCountry = country
        print("Internal: \(aCity), \(aState) in \(aCountry)")    }
    else {
        print("Error: Unable to retrieve address for postal code \(postalCode)")
    }
}

print("External: \(aCity), \(aState) in \(aCountry)")

Here are the results I get:

External: ,  in 
Internal: New York, NY in United States
indyMac
  • 17
  • 3

2 Answers2

1

The getAddress is an asynchronous function. It returns immediately, but its completion closure is called asynchronously (i.e., later). Your three variables are not populated by the time getAddress returns, but only later. This is how the asynchronous completion-handler pattern works. If you search the web for “swift completion handler”, you will find many good discussions on this topic.

It’s also one of the reasons that async-await of Swift concurrency is so attractive, that it eliminates this silliness. If you find this completion handler pattern confusing, consider adopting Swift concurrency.

If you are interested in learning about Swift concurrency, I might suggest watching WWDC 2021 video Meet async/await in Swift. On that page, there are links to other Swift concurrency videos, too.


For the sake of comparison, there is an async rendition of geocodeAddressString. E.g.:

func foo() async {
    do {
        let result = try await address(forPostalCode: "10001")
        print(result)
    } catch {
        print(error)
    }
}

func address(forPostalCode postalCode: String) async throws -> Address {
    try await geocodeAddressString("\(postalCode), USA")
}

func geocodeAddressString(_ string: String) async throws -> Address {
    let geocoder = CLGeocoder()

    guard let placemark = try await geocoder.geocodeAddressString(string).first else {
        throw CLError(.geocodeFoundNoResult)
    }

    guard let city = placemark.locality else {
        throw AddressError.noCity
    }

    guard let state = placemark.administrativeArea else {
        throw AddressError.noState
    }

    guard let country = placemark.country else {
        throw AddressError.noCountry
    }

    return Address(city: city, state: state, country: country)
}

This eliminates the complicated reasoning that the completion handler closure pattern entails. It gives you code that is linear and logical in its flow, but also is asynchronous and avoids blocking any threads.

As an aside, note that I chose to avoid the three separate variables of city, state, and zip and wrapped that in an Address object:

struct Address {
    let city: String
    let state: String
    let country: String
}

Note, I also avoid the use of NSError and use my own custom error type.

enum AddressError: Error {
    case noCity
    case noState
    case noCountry
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
0

The print with "External:" is run before the completion was called. That is why it is printed first.

The completion function is called asynchronously and returns at some point in the future, which could be fast, but is (in your case) not before the External print.

So,

  1. getAddress is called
  2. geocoder.geocodeAddressString inside of that is called asynchronously
  3. getAddress returns
  4. The print "External" is run
  5. geocoder.geocodeAddressString calls the completion function
  6. print "Internal" is run (and the variables are set)

The variables don't "disappear" -- at the point that you printed them, they weren't set yet.

Whatever effect you want to happen needs to be started after the completion is called.

Lou Franco
  • 87,846
  • 14
  • 132
  • 192