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