DEV Community

Andrew Lundy
Andrew Lundy

Posted on

Core Location - How to Display a Human-Readable Address Using CLGeocoder

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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)"
    }
}
Enter fullscreen mode Exit fullscreen mode

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)"
        }
    } 
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)