Search code examples
swiftclgeocoder

How do I get the results from an @escaping closure to not disappear


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

Solution

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