Andrew Lundy
Posted on September 6, 2020
Exordium
In a previous post, I wrote about setting up Core Location in a UIKit application. I covered adding the usage descriptions to the Info.plist file, requesting location authorization, and pulling the location from the CLLocationManager
. If you need some help setting up Core Location in your UIKit app, take a look at that post: https://rustynailsoftware.com/dev-blog/core-location-setting-up-core-location-with-uikit.
In this post, I'll be going over how to implement CLGeocoder
- a class in Core Location that helps developers produce human-readable versions of geographic coordinates in their iOS apps. I'll also briefly cover the CLLocation
object, as I failed to do so in my first Core Location post. You can follow along with the code I've hosted on GitHub: https://github.com/andrew-lundy/core-location-tutorial
Let's dive in.
The CLLocation Class
An essential aspect of Core Location is the CLLocation
class. It's so essential that I forgot to write about it in my first Core Location series blog post. The CLLocation
class is an object that holds the device's location information - including the altitude and course information. The course info is the speed and direction of a device. In Core Location, you obtain the location details via the CLLocationManager
class. Here, I've stored the information in the currentLocation
variable:
let locationManager = CLLocationManager()
let currentLocation = locationManager.location
With this, we have gained access to the location details and the ability to pull those details. For example, to obtain the coordinates of the location, use the location manager's coordinate
attribute:
let locationManager = CLLocationManager()
let currentLocation = locationManager.location
// Print the location details.
// Ex: <+37.78735352, -122.40822700> +/- 5.00m (speed - 1.99 mps / course - 1.00) @ 9/5/20, 5:13:46 PM Central Daylight Time
print(currentLocation)
let locationCoordinate = currentLocation.coordinate
// Print the coordinate value of the location as a CLLocationCoordinate2D.
// Ex: CLLocationCoordinate2D(latitude: 37.787353515625, longitude: -122.408227)
print(locationCoordinate)
Reverse-Geocoding with CLGeocoder
As seen above, the CLLocation
class returns the location's information in a pretty much non-usable format. Sure, we can pull the geographic coordinates and the speed at which the device is moving. But, this information can only be useful in certain situations. What happens when we need to display the location info to users who don't want to read and convert coordinates? The answer is found in Apple's CLGeocoder
class.
As of now, the ViewController
class holds the following objects and IBOutlets
:
@IBOutlet weak var changeLocationBttn: UIButton!
@IBOutlet weak var reverseGeocodeLocation: UIButton!
@IBOutlet weak var locationDataLbl: UILabel!
private var locationManager: CLLocationManager!
private var currentLocation: CLLocation!
private var geocoder: CLGeocoder!
I am going to go ahead and initialize the CLGeocoder in the viewDidLoad method of the ViewController class:
override func viewDidLoad() {
super.viewDidLoad()
changeLocationBttn.layer.cornerRadius = 10
reverseGeocodeLocation.layer.cornerRadius = 10
reverseGeocodeLocation.titleLabel?.textAlignment = .center
locationManager = CLLocationManager()
locationManager.delegate = self
// Initialize the Geocoder
geocoder = CLGeocoder()
}
All of the work that the CLGeocoder
will be doing is going to happen in the reverseGeocodeLocationBttnTapped
method. The first thing we are going to do is make sure that the currentLocation
variable is not empty. This is set up to hold the device's location information and is given a value when the app requests authorization status. We need to make this check because there is nothing to reverse-geocode if there is no location value.
@IBAction func reverseGeocodeLocationBttnTapped(_ sender: Any) {
guard let currentLocation = self.currentLocation else {
print("Unable to reverse-geocode location.")
return
}
}
To start the process of reverse-geocoding coordinates, you must call the reverseGeocodeLocation method on the CLGeocoder
. This method takes two parameters - a CLLocation
object and a CLGeocodeCompletionHandler
. The completion handler also has two parameters - an array of CLPlacemark
and an Error
.
// The method that does the reverse-geocoding.
geocoder.reverseGeocodeLocation(location: CLLocation, completionHandler: CLGeocodeCompletionHandler)
// Here is the method when in use.
geocoder.reverseGeocodeLocation(currentLocation) { (placemarks, error) in
}
The CLPlacemark
class is new, so let's take a look at it. A CLPlacemark
, or 'placemark,' holds the human-readable version of a coordinate and gives developers access to information such as the name of a place, the city, state, zip code, and more. You can read more about the CLPlacemark
class in Apple's docs: https://developer.apple.com/documentation/corelocation/clplacemark
Here are the steps we'll perform in the completion handler:
geocoder.reverseGeocodeLocation(currentLocation) { (placemarks, error) in
// 1
if let error = error {
print(error)
}
// 2
guard let placemark = placemarks?.first else { return }
print(placemark)
// Geary & Powell, Geary & Powell, 299 Geary St, San Francisco, CA 94102, United States @ <+37.78735352,-122.40822700> +/- 100.00m, region CLCircularRegion (identifier:'<+37.78735636,-122.40822737> radius 70.65', center:<+37.78735636,-122.40822737>, radius:70.65m)
// 3
guard let streetNumber = placemark.subThoroughfare else { return }
guard let streetName = placemark.thoroughfare else { return }
guard let city = placemark.locality else { return }
guard let state = placemark.administrativeArea else { return }
guard let zipCode = placemark.postalCode else { return }
// 4
DispatchQueue.main.async {
self.locationDataLbl.text = "\(streetNumber) \(streetName) \n \(city), \(state) \(zipCode)"
}
}
Check if the handler produces an error. If so, print it to the console. In a real app, you'd handle the error more efficiently.
Use a guard statement to obtain the first placemark returned from the completion handler. For most geocoding requests, the array of placemarks should only contain one entry. I went ahead and printed the placemark data.
Pull specific data out of the placemark, depending on the use case. In this instance, I've pulled the street number, street name, city, state, and zip code.
Finally, I've updated the label in the app with the placemark data. Since this is changing the user interface, I have done this on the main thread using the DispatchQueue
class.
The reverseGeocodeLocationBttnTapped
IBAction
should now look like this:
@IBAction func reverseGeocodeLocationBttnTapped(_ sender: Any) {
guard let currentLocation = self.currentLocation else {
print("Unable to reverse-geocode location.")
return
}
geocoder.reverseGeocodeLocation(currentLocation) { (placemarks, error) in
if let error = error {
print(error)
}
guard let placemark = placemarks?.first else { return }
guard let streetNumber = placemark.subThoroughfare else { return }
guard let streetName = placemark.thoroughfare else { return }
guard let city = placemark.locality else { return }
guard let state = placemark.administrativeArea else { return }
guard let zipCode = placemark.postalCode else { return }
DispatchQueue.main.async {
self.locationDataLbl.text = "\(streetNumber) \(streetName) \n \(city), \(state) \(zipCode)"
}
}
}
Posted on September 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.