Home iOS & Swift Books UIKit Apprentice

23
Use Location Data Written by Matthijs Hollemans & Fahim Farook

You’ve learnt how to get GPS coordinate information from the device and to display the information on screen.

In this chapter, you will learn the following:

  • Handle GPS errors: Receiving GPS information is an error-prone process. How do you handle the errors?
  • Improve GPS results: How to improve the accuracy of the GPS results you receive.
  • Reverse geocoding: Getting the address for a given set of GPS coordinates.
  • Testing on device: Testing on device to ensure that your app handles real-world scenarios.
  • Support different screen sizes: Setting up your UI to work on iOS devices with different screen sizes.

Handle GPS errors

Getting GPS coordinates is error-prone. You may be somewhere where there is no clear line-of-sight to the sky — such as inside or in an area with lots of tall buildings — blocking your GPS signal.

There may not be many Wi-Fi routers around you, or they haven’t been catalogued yet, so the Wi-Fi radio isn’t much help getting a location fix either.

And of course your cellular signal might be so weak that triangulating your position doesn’t offer particularly good results either.

All of that is assuming your device actually has a GPS or cellular radio. I just went out with my iPod touch to capture coordinates and get some pictures for this app. In the city center it was unable to obtain a location fix. My iPhone did better, but it still wasn’t ideal.

The moral of this story is that your location-aware apps had better know how to deal with errors and bad readings. There are no guarantees that you’ll be able to get a location fix, and if you do, then it might still take a few seconds.

This is where software meets the real world. You should add some error handling code to the app to let users know about problems getting those coordinates.

The error handling code

➤ Add these two instance variables to CurrentLocationViewController.swift:

var updatingLocation = false
var lastLocationError: Error?
func locationManager(
  _ manager: CLLocationManager, 
  didFailWithError error: Error
) {
  print("didFailWithError \(error.localizedDescription)")

  if (error as NSError).code == CLError.locationUnknown.rawValue {
    return
  }
  lastLocationError = error
  stopLocationManager()
  updateLabels()
}
if (error as NSError).code == CLError.locationUnknown.rawValue {
  return
}
lastLocationError = error
stopLocationManager()

Stop location updates

If obtaining a location appears to be impossible for wherever the user currently is on the globe, then you need to tell the location manager to stop. To conserve battery power, the app should power down the iPhone’s radios as soon as it doesn’t need them anymore.

func stopLocationManager() {
  if updatingLocation {
    locationManager.stopUpdatingLocation()
    locationManager.delegate = nil
    updatingLocation = false
  }
}
func updateLabels() {
  if let location = location {
    . . .
  } else {
    . . .
    // Remove the following line
    messageLabel.text = "Tap 'Get My Location' to Start"
    // The new code starts here:
    let statusMessage: String
    if let error = lastLocationError as NSError? {
      if error.domain == kCLErrorDomain && error.code == CLError.denied.rawValue {
        statusMessage = "Location Services Disabled"
      } else {
        statusMessage = "Error Getting Location"
      }
    } else if !CLLocationManager.locationServicesEnabled() {
      statusMessage = "Location Services Disabled"
    } else if updatingLocation {
      statusMessage = "Searching..."
    } else {
      statusMessage = "Tap 'Get My Location' to Start"
    }
    messageLabel.text = statusMessage
  }
}

Start location updates

➤ Also add a new startLocationManager() method — I suggest you put it right above stopLocationManager(), to keep related functionality together:

func startLocationManager() {
  if CLLocationManager.locationServicesEnabled() {
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
    locationManager.startUpdatingLocation()
    updatingLocation = true
  }
}
@IBAction func getLocation() {
  . . .
  if authStatus == .denied || authStatus == .restricted {
    . . .
  }
  // New code below, replacing existing code after this point
  startLocationManager()
  updateLabels()
}
lastLocationError = nil
The app is waiting to receive GPS coordinates
Mjo ozv ef xeoyext qa huwoija MYH yaelgixuqoh

Simulating locations from within the Xcode debugger
Rexawivikb dodexeopb ytoj suxmir mga Vzoxe pucugrit

Improve GPS results

Cool, you know how to obtain a CLLocation object from Core Location and you’re able to handle errors. Now what?

Get results for a specific accuracy level

➤ Change locationManager(_:didUpdateLocations:) to the following:

func locationManager(
  _ manager: CLLocationManager, 
  didUpdateLocations locations: [CLLocation]
) {
  let newLocation = locations.last!
  print("didUpdateLocations \(newLocation)")

  // 1
  if newLocation.timestamp.timeIntervalSinceNow < -5 {
    return
  }

  // 2
  if newLocation.horizontalAccuracy < 0 {
    return
  }

  // 3
  if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {

    // 4
    lastLocationError = nil
    location = newLocation

    // 5
    if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
      print("*** We're done!")
      stopLocationManager()
    }
    updateLabels()
  }
}

Short circuiting

Because location is an optional object, you cannot access its properties directly — you first need to unwrap it. You could do that with if let, but if you’re sure that the optional is not nil you can also force unwrap it with !.

if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {

Update the UI

To make this clearer, you are going to toggle the Get My Location button to say “Stop” when the location grabbing is active and switch it back to “Get My Location” when it’s done. That gives a nice visual clue to the user. Later on, you’ll also show an animated activity spinner that makes this even more obvious.

func configureGetButton() {
  if updatingLocation {
    getButton.setTitle("Stop", for: .normal)
  } else {
    getButton.setTitle("Get My Location", for: .normal)
  }
}
func updateLabels() {
  . . .
  configureGetButton()
}
if updatingLocation {
  stopLocationManager()
} else {
  location = nil
  lastLocationError = nil
  startLocationManager()
}

Reverse geocoding

The GPS coordinates you’ve dealt with so far are just numbers. The coordinates 37.33240904, -122.03051218 don’t really mean that much, but the address 1 Infinite Loop in Cupertino, California does.

The implementation

➤ Add the following properties to CurrentLocationViewController.swift:

let geocoder = CLGeocoder()
var placemark: CLPlacemark?
var performingReverseGeocoding = false
var lastGeocodingError: Error?
func locationManager(
  _ manager: CLLocationManager, 
  didUpdateLocations locations: [CLLocation]
) {
  . . .
  if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
    . . .
    if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
       . . .
    }
    updateLabels()
    // The new code begins here:
    if !performingReverseGeocoding {
      print("*** Going to geocode")

      performingReverseGeocoding = true

      geocoder.reverseGeocodeLocation(newLocation) {placemarks, error in
        if let error = error {
          print("*** Reverse Geocoding error: \(error.localizedDescription)")
          return
        }
        if let places = placemarks {
          print("*** Found places: \(places)")
        }
      }
    }
    // End of the new code
  }
}

Closures

Unlike the location manager, CLGeocoder does not use a delegate to return results from an operation. Instead, it uses a closure. Closures are an important Swift feature and you can expect to see them all over the place — for Objective-C programmers, a closure is similar to a “block”.

geocoder.reverseGeocodeLocation(newLocation) {placemarks, error in
  // put your statements here
}
{ placemarks, error in
    // put your statements here
}
didUpdateLocations <+37.33233141,-122.03121860> +/- 5.00m (speed 0.00 mps / course -1.00) @ 8/11/20, 5:01:49 PM Eastern Daylight Time
*** Going to geocode
*** Found places: [Apple Campus, Apple Campus, 1 Infinite Loop, Cupertino, CA  95014, United States @ <+37.33233141,-122.03121860> +/- 100.00m, region CLCircularRegion (identifier:'<+37.33213110,-122.02990105> radius 279.38', center:<+37.33213110,-122.02990105>, radius:279.38m)]

Handle reverse geocoding errors

➤ Replace the contents of the geocoding closure with the following:

self.lastGeocodingError = error
if error == nil, let places = placemarks, !places.isEmpty {
  self.placemark = places.last!
} else {
  self.placemark = nil
}

self.performingReverseGeocoding = false
self.updateLabels()
if error == nil, let places = placemarks, !places.isEmpty {
if there’s no error and the unwrapped placemarks array is not empty {
if error == nil {
  if let places = placemarks {
    if !places.isEmpty {
  self.placemark = places.last!

Display the address

Let’s show the address to the user.

func updateLabels() {
  if let location = location {
    . . .
    // Add this block
    if let placemark = placemark {
      addressLabel.text = string(from: placemark)
    } else if performingReverseGeocoding {
      addressLabel.text = "Searching for Address..."
    } else if lastGeocodingError != nil {
      addressLabel.text = "Error Finding Address"
    } else {
      addressLabel.text = "No Address Found"
    }
    // End new code
  } else {
    . . .
  }
}
func string(from placemark: CLPlacemark) -> String {
  // 1
  var line1 = ""
  // 2
  if let tmp = placemark.subThoroughfare {
    line1 += tmp + " "
  }
  // 3
  if let tmp = placemark.thoroughfare {
    line1 += tmp
  }
  // 4
  var line2 = ""
  if let tmp = placemark.locality {
    line2 += tmp + " "
  }
  if let tmp = placemark.administrativeArea {
    line2 += tmp + " "
  }
  if let tmp = placemark.postalCode {
    line2 += tmp
  }
  // 5
  return line1 + "\n" + line2
}
placemark = nil
lastGeocodingError = nil
Reverse geocoding finds the address for the GPS coordinates
Todotgi saemeziff xajrv fro imtsowt lep jda RRD muonqofewip

Testing on device

When I first wrote this code, I had only tested it on the Simulator. It worked fine there. Then, I put it on my iPod touch and guess what? Not so good.

First fix

➤ Change locationManager(_:didUpdateLocations:) to:

func locationManager(
  _ manager: CLLocationManager, 
  didUpdateLocations locations: [CLLocation]
) {
  . . .

  if newLocation.horizontalAccuracy < 0 {
    return
  }

  // New section #1
  var distance = CLLocationDistance(Double.greatestFiniteMagnitude)
  if let location = location {
    distance = newLocation.distance(from: location)
  }
  // End of new section #1
  if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
    . . .
    if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
      . . .
      // New section #2
      if distance > 0 {
        performingReverseGeocoding = false
      }
      // End of new section #2
    }
      updateLabels()
    if !performingReverseGeocoding {
      . . .
    }

  // New section #3
  } else if distance < 1 {
    let timeInterval = newLocation.timestamp.timeIntervalSince(location!.timestamp)
    if timeInterval > 10 {
      print("*** Force done!")
      stopLocationManager()
      updateLabels()
    }
    // End of new sectiton #3
  }
}
var distance = CLLocationDistance(Double.greatestFiniteMagnitude)
if let location = location {
  distance = newLocation.distance(from: location)
}
if distance > 0 {
  performingReverseGeocoding = false
}
} else if distance < 1 {
  let timeInterval = newLocation.timestamp.timeIntervalSince(location!.timestamp)
  if timeInterval > 10 {
    print("*** Force done!")
    stopLocationManager()
    updateLabels()
  }
}
} else if distance == 0 {

Second fix

There is another improvement you can make to increase the robustness of this logic, and that is to set a time-out on the whole thing. You can tell iOS to perform a method one minute from now. If by that time the app hasn’t found a location yet, you stop the location manager and show an error message.

var timer: Timer?
func startLocationManager() {
  if CLLocationManager.locationServicesEnabled() {
    . . .
    timer = Timer.scheduledTimer(
      timeInterval: 60,
      target: self,
      selector: #selector(didTimeOut),
      userInfo: nil,
      repeats: false)
  }
}
func stopLocationManager() {
  if updatingLocation {
    . . .
    if let timer = timer {
      timer.invalidate()
    }
  }
}
@objc func didTimeOut() {
  print("*** Time out")
  if location == nil {
    stopLocationManager()
    lastLocationError = NSError(
      domain: "MyLocationsErrorDomain", 
      code: 1, 
      userInfo: nil)
    updateLabels()
  }
}
The error after a time out
Mbe oppij utyih o zenu air

Required device capabilities

The Info.plist file has a key, Required device capabilities, that lists the hardware that your app needs in order to run. This is the key that the App Store uses to determine whether a user can install your app on their device.

Adding location-services to Info.plist
Ubxakv luwunias-mevzivas mo Eqvi.wvetp

Attributes and properties

Most of the attributes in Interface Builder’s inspectors correspond directly to properties on the selected object. For example, a UILabel has the following attributes:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.