Leave a rating/review
Notes: 17. Geocode an Address
The student materials have been reviewed and are updated as of January, 2022.
Geocoding is process of converting addresses into coordinates and coordinates into addresses. To do this, we use an object called the CLGeocoder.
Since we can’t store the addresses of every coordinate into our phone, we must rely on the network to make such a request. Thankfully, the GLencoder object takes care of this for us. We don’t need to worry about business of networking in this request. We just need to provide a completion handler. The handler will either return to us the result of our query in the form of CLPlacemark object, or we’ll need to respond to an error. Since we’re dealing with network requests, you must always respond to these errors as your user may try and make a request while in airplane mode or they may lose connection in the middle of a request. Just a few notes about lookups.
You need to make them sparingly because they are rate limited. Make too many requests in a short period of time, apple will return deny your requests with a kCLErrorNetwork object. Essentially, you want to make one lookup per user action.
If your user is moving and you want to make a lookup request, make sure a significant distance has been traveled from your last lookup or significant amount of time has passed such as a minute.
Finally, always make a lookup when the user can immediately see the results of that lookup. You never want to make a lookup when the app is in the background.
There are two types of lookups that you can make. The first kind is called forward lookup. Simply pass in a string with the address and the Geocoder will do its best to return the latitude and longitude of the address. It may also return additional information such as a point of interest or a building at that location. The method you’ll use is called geocodeAddressString() and provide a completion handler.
To get the address from latitude and longitude, you’ll make a reverse geocoding request. To make this request, you must first take your coordinates and create a CLLocation object. Then, you’ll call the method reverseGeocodeLocation passing in this location object.
Both of these methods have a completion handler that returns an array of CLPlacemarks. Most of the time, only one object will be returned, but in cases where the system cannot determine the actual location, then several placemarks may be returned. At that point, it’s up to you to determine the actual location.
As mentioned, CLPlacemarks is the object returned from Geocoding. This objects not only encapsulate a CLLocation object, but also the address of the location and some additional properties such as if there is an ocean associated with the location or if there are any nearby areas of interest.
Since there are many ways of managing addresses throughout the world, they are suitably abstracted in the CLPlacemark object. For instance, the street number is accessed by subThroughFare property whereas the street itself is called thoroughfare. To find the name of the city, youíd use the locality property whereas the state or province is found under the administrativeArea property. As always, when in doubt, check the documentation.
Demo
To get started, open up your demo sample app. Our app has an issue. It doesn’t display the addresses of all the various locations. All we have is a latitude and longitude coordinate. This won’t be a problem. Open up your LocationManager.swift. We’ll start by adding a current addresses property.
@Published var currentAddress = ""
We also need to CLGeocoder object. We’ll make it lazy so that we can initialize it when we actually need it. Add the following.
lazy var geocoder = CLGeocoder()
This will be last address submitted for a reverse geocode lookup. Next, let’s add a new method called, “fetch address”. It will take a place for the lookup.
func fetchAddress(for place: Place) {
}
First, we’ll set the current address to be empty.
currentAddress = ""
Now we do reverseGoecodeLocation passing in our location. This brings a closure that provides an array of placemarks and an error.
geocoder.reverseGeocodeLocation(place.location) { [weak self] placemarks, error in
}
Next we check to see if there are any errors. If so, we’ll crash with a fatalError.
if let error = error {
fatalError(error.localizedDescription)
}
Now we can access the placemarks. We’ll first use a guard statement to unwrap it.
guard let placemark = placemarks?.first else { return }
Using the placemarks, we’ll create an address. We’ll do this in an if-let statement.
if let streetNumber = placemark.subThoroughfare,
let street = placemark.thoroughfare,
let city = placemark.locality,
let state = placemark.administrativeArea {
self?.currentAddress = "\(streetNumber) \(street) \(city), \(state)"
}
Of course, some of these places may not have a traditional address. In that case, we’ll just print out the city and the state.
else if let city = placemark.locality, let state = placemark.administrativeArea {
self.currentAddress = "\(city), \(state)"
}
Finally, if that doesn’t work, we’ll simply print out a generic address unknown.
else {
self.currentAddress = "Address Unknown"
}
And that’s it. Now let’s show the address. In the Views group, open ContentView.swift. At the top, we’ll need to add our location manager. We’ll make this a StateObject.
@StateObject private var locationManager = LocationManager()
We’ll need to pass this object through the rest of the views. Now add the property in PlacesView.
@ObservedObject var locationManager: LocationManager
As well as add it to the OtherPlacesScrollView.
@ObservedObject var locationManager: LocationManager
Now let’s pass it through the view hierarchy. In ContentView first we’ll pass it to the PlacesView.
PlacesView(places: places, selectedPlace: $selectedPlace, locationManager: locationManager)
And now we’ll pass it to the OtherPlacesScrollView.
OtherPlacesScrollView(selectedPlace: $selectedPlace, locationManager: locationManager, places: places)
Every time the selected place is changed, we should fetch the address of the new location. Now we need to update the view. Scroll down to the OtherPlacesScrollView struct. We’ll fetch the address once the place has changed.
@Binding var selectedPlace: Place {
didSet {
locationManager.fetchAddress(for: selectedPlace)
}
}
Next, we need to fetch our addresses on startup. In ContentView, add the following to the NavigationView.
.onAppear {
locationManager.fetchAddress(for: place)
}
Now, let’s up the address string. Scroll to the PlacesView and replace the Address string with the string fetched from the location manager.
Text(locationManager.currentAddress)
Now before we build and run, we need to update set the ContentView in the app. Open InterestingPlacesApp.swift and delete the LocationView and replace it with ContentView. That’s it. Now we get addresses. Build and run. You’ll see that addresses appear when you select a place.