Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

28. Use Location Data
Written by Eli Ganim

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

You’ve learned 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.

Handling 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()

Stopping 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
  }
}

Starting 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
Txo irb oq lianixs za qoyauje MVG tuekkasocix

Simulating locations from within the Xcode debugger
Jekibuwepm pozaweuvq btic morvav mla Wruje pepawfin

Improving GPS results

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

Getting 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 {

Updating 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()
}
The stop button
Gje xqev vawkoc

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?
if !performingReverseGeocoding {
  print("*** Going to geocode")

  performingReverseGeocoding = true

  geocoder.reverseGeocodeLocation(newLocation,
                                  completionHandler: {
    placemarks, error in
    if let error = error {
      print("*** Reverse Geocoding error: \(error.localizedDescription)")
      return
    }
    if let places = placemarks {
      print("*** Found places: \(places)")
    }
  })
}

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, completionHandler:
{ placemarks, error in
  // put your statements here
}
{ placemarks, error in
    // put your statements here
}
didUpdateLocations <+37.33233141,-122.03121860> +/- 379.75m (speed -1.00 mps / course -1.00) @ 7/1/17, 10:31:15 AM Israel Daylight Time
*** Going to geocode
*** Found places: [Apple Inc., Apple Inc., 1 Infinite Loop, Cupertino, CA  95014, United States @ <+37.33233141,-122.03121860> +/- 100.00m, region CLCircularRegion (identifier:’<+37.33233140,-122.03121860> radius 141.73’, center:<+37.33233140,-122.03121860>, radius:141.73m)]

Handling reverse geocoding errors

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

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

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

Displaying 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 s = placemark.subThoroughfare {
    line1 += s + " "
  }

  // 3
  if let s = placemark.thoroughfare {
    line1 += s
  }

  // 4
  var line2 = ""

  if let s = placemark.locality {
    line2 += s + " "
  }
  if let s = placemark.administrativeArea {
    line2 += s + " "
  }
  if let s = placemark.postalCode {
    line2 += s
  }
  
  // 5
  return line1 + "\n" + line2
}
placemark = nil
lastGeocodingError = nil
Reverse geocoding finds the address for the GPS coordinates
Gahoyqa bueyavezx dojbd rfu abnnewn rul bfi XKX geisbamafow

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

➤ First add a new instance variable:

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
Wze ihtip inyan u joda uaf

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
Ilgivb hiwakaaz-loxkudob ja Acwo.bviqp

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now