The GeocodingClient class returns stub data for the time being. In this episode, we integrate the Core Location framework and take advantage of the geocoding APIs it offers. We use the geocoding APIs to forward geocode the address the user enters in the TextField of the AddLocationView to a collection of Location objects.
Forward Geocoding with CLGeocoder
Open GeocodingClient.swift and replace the import statement for Foundation with an import statement for Core Location.
import CoreLocation
final class GeocodingClient: GeocodingService {
...
}
The geocodeAddressString(_:) method is throwing, but we don't want to expose the errors the Core Location framework throws to the call site. For that reason, we declare a type that defines the errors the geocodeAddressString(_:) method can throw. Add a nested enum with name GeocodingError that conforms to the Error protocol. We add cases to the GeocodingError enum as we implement the GeocodingClient class.
import CoreLocation
final class GeocodingClient: GeocodingService {
// MARK: - Types
enum GeocodingError: Error {
}
...
}
The type that does the heavy lifting is the CLGeocoder class of the Core Location framework. Define a private, constant property geocoder of type CLGeocoder. We create a CLGeocoder instance and store a reference to the instance in the geocoder property.
import CoreLocation
final class GeocodingClient: GeocodingService {
// MARK: - Types
enum GeocodingError: Error {
}
// MARK: - Properties
private let geocoder = CLGeocoder()
...
}
It is time to implement the geocodeAddressString(_:) method. We use a guard statement to verify that the string stored in the addressString parameter isn't empty. That may seem redundant because we already filter out empty strings in the AddLocationViewModel class. That is true, but the GeocodingClient class is unaware of the AddLocationViewModel class and we cannot predict how or where the GeocodingClient class will be used in the future.
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
guard !addressString.isEmpty else {
}
}
In the else clause of the guard statement, we throw an error. Add a case to the GeocodingError enum with name invalidAddressString.
enum GeocodingError: Error {
case invalidAddressString
}
In the else clause of the guard statement, we throw an invalidAddressString error.
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
guard !addressString.isEmpty else {
throw GeocodingError.invalidAddressString
}
}
The method we invoke to geocode an address is throwing so we wrap it in a do-catch statement. The method we use to forward geocode the address is named geocodeAddressString(_:). It accepts a string as its only argument and returns an array of CLPlacemark instances. The method is asynchronous and throwing so we prefix the method call with the try and await keywords. We store the result in a constant with name placemarks.
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
guard !addressString.isEmpty else {
throw GeocodingError.invalidAddressString
}
do {
let placemarks = try await geocoder.geocodeAddressString(addressString)
} catch {
}
}
A CLPlacemark instance is a description of a location. The Core Location framework is a dependency of the GeocodingClient class and I want to keep it that way. For that reason, we convert the array of CLPlacemark instances to an array of Location objects.
We invoke the compactMap(_:) method on the array of placemarks. Why we choose for the compactMap(_:) method and not the map(_:) method becomes clear in a moment. The closure we pass to the compactMap(_:) method accepts a CLPlacemark instance as an argument and it returns an optional Location object.
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
guard !addressString.isEmpty else {
throw GeocodingError.invalidAddressString
}
do {
let placemarks = try await geocoder.geocodeAddressString(addressString)
return placemarks.compactMap { placemark -> Location? in
}
} catch {
}
}
A placemark has an optional name, an optional country, and an optional location. These values are required to create a Location object so we use a guard statement to safely access these values. We are only interested in the coordinates of the location. In the else clause of the guard statement, we return nil. That is why we chose for the compactMap(_:) method and not the map(_:) method.
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
guard !addressString.isEmpty else {
throw GeocodingError.invalidAddressString
}
do {
let placemarks = try await geocoder.geocodeAddressString(addressString)
return placemarks.compactMap { placemark -> Location? in
guard
let name = placemark.name,
let country = placemark.country,
let coordinate = placemark.location?.coordinate
else {
return nil
}
}
} catch {
}
}
We use the values to create a Location object. The identifier of the Location object is a randomly generated UUID. We return the Location object from the closure we pass to the compactMap(_:) method and we return the array of Location objects from the geocodeAddressString(_:) method
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
guard !addressString.isEmpty else {
throw GeocodingError.invalidAddressString
}
do {
let placemarks = try await geocoder.geocodeAddressString(addressString)
return placemarks.compactMap { placemark -> Location? in
guard
let name = placemark.name,
let country = placemark.country,
let coordinate = placemark.location?.coordinate
else {
return nil
}
return Location(
id: UUID().uuidString,
name: name,
country: country,
latitude: coordinate.latitude,
longitude: coordinate.longitude
)
}
} catch {
}
}
In the catch clause, we print the address and the error to the console. We also need to signal to the call site that the request failed. Add a case to the GeocodingError enum with name requestFailed.
enum GeocodingError: Error {
case invalidAddressString
case requestFailed
}
In the catch clause, we throw a requestFailed error.
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
guard !addressString.isEmpty else {
throw GeocodingError.invalidAddressString
}
do {
let placemarks = try await geocoder.geocodeAddressString(addressString)
return placemarks.compactMap { placemark -> Location? in
guard
let name = placemark.name,
let country = placemark.country,
let coordinate = placemark.location?.coordinate
else {
return nil
}
return Location(
id: UUID().uuidString,
name: name,
country: country,
latitude: coordinate.latitude,
longitude: coordinate.longitude
)
}
} catch {
print("Unable to Geocode Address String \(error)")
throw GeocodingError.requestFailed
}
}
Implementing a Failable Initializer
We can improve the implementation of the GeocodingClient class a little by implementing an initializer for the Location struct that accepts a CLPlacemark instance. Open Location.Swift, add an import statement for the Core Location framework at the top, and create an extension for the Location struct. We define a failable initializer that accepts a CLPlacemark instance.
import CoreLocation
struct Location: Codable {
...
}
extension Location {
init?(placemark: CLPlacemark) {
}
}
Open GeocodingClient.swift and move the guard statement of the closure we pass to the compactMap(_:) method to the failable initializer. We use the values to set the properties of the Location object. That's it.
extension Location {
init?(placemark: CLPlacemark) {
guard
let name = placemark.name,
let country = placemark.country,
let coordinate = placemark.location?.coordinate
else {
return nil
}
id = UUID().uuidString
self.name = name
self.country = country
latitude = coordinate.latitude
longitude = coordinate.longitude
}
}
With the failable initializer in place, we can clean up the implementation of the geocodeAddressString(_:) method of the GeocodingClient class. We invoke the compactMap(_:) method on the return value of the geocodeAddressString(_:) method of the CLGeocoder instance. We don't pass a closure to the compactMap(_:) method. Instead we pass a reference to the failable initializer we implemented a moment ago and return the array of Location objects from the geocodeAddressString(_:) method.
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
guard !addressString.isEmpty else {
throw GeocodingError.invalidAddressString
}
do {
return try await geocoder
.geocodeAddressString(addressString)
.compactMap(Location.init(placemark:))
} catch {
print("Unable to Geocode Address String \(error)")
throw GeocodingError.requestFailed
}
}
Run the application to see the result of the changes we made. Navigate to the AddLocationView and enter the name of a town or city in the TextField. The Core Location API is quite fast so the results should be visible almost immediately.

What's Next?
I hope you agree that integrating the Core Location framework wasn't difficult. I want to point out that we are not able to reliably unit test the GeocodingClient class because it is tightly coupled to the Core Location framework. We can resolve this by using the same pattern we used a few times in this series. That is the focus of the next episode.