Core Location Tutorial for iOS: Tracking Visited Locations

In this Core Location tutorial, you will learn how to use visit monitoring to track a user’s visited locations. By Andrew Kharchyshyn.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Choosing the Most Appropriate Locations Data

The Core Location framework has many ways to track a user’s location and each has different characteristics:

  • Standard location services: High battery impact. High location precision. Great for navigational or fitness apps.
  • Significant location changes: Medium battery impact. Medium location precision. Low stops precision.
  • Regional monitoring: Low battery impact. Great location precision. Requires specific regions in order to monitor.

None of these is fully suitable for your app. Low battery impact is a must — a user is unlikely to use the app otherwise. What’s more, regional monitoring is also undesirable because you limit user movement to some specific regions.

Fortunately, there is one other API you can use.

Visit Monitoring

Visit monitoring allows you to track destinations — places where the user stops for a while. It wakes the app whenever a new visit is detected and is very energy efficient and not tied to any landmark.

Subscribe to Location Changes

Now that you know which of the many Core Location APIs you’ll use to get the user’s location, it’s time to start implementing it!

CLLocationManager

In the AppDelegate.swift, below this line:

locationManager.requestAlwaysAuthorization()

add the following code:

locationManager.startMonitoringVisits()
locationManager.delegate = self

The first line initiates the listening feature. Core Location uses delegate callbacks to inform you of location changes.

Now, add this extension at the bottom of the file:

extension AppDelegate: CLLocationManagerDelegate {
  func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
    // create CLLocation from the coordinates of CLVisit
    let clLocation = CLLocation(latitude: visit.coordinate.latitude, longitude: visit.coordinate.longitude) 

    // Get location description
  }

  func newVisitReceived(_ visit: CLVisit, description: String) {
    let location = Location(visit: visit, descriptionString: description)

    // Save location to disk
  }
}

The first method is the callback from CLLocationManager when the new visit is recorded and it provides you with a CLVisit.

CLVisit has four properties:

  1. arrivalDate: The date when the visit began.
  2. departureDate: The date when the visit ended.
  3. coordinate: The center of the region that the device is visiting.
  4. horizontalAccuracy: An estimate of the radius (in meters) of the region.

You need to create a Location object from this data and, if you recall, there is an initializer that takes the CLVisit, date and description string:

init(_ location: CLLocationCoordinate2D, date: Date, descriptionString: String)

The only thing missing in the above is descriptionString.

Location Description

To get the description, you will use CLGeocoder. Geocoding is the process of converting coordinates into addresses or place names in the real world. If you want to get an address from a set of coordinates, you use reverse geocoding. Thankfully, Core Location gives us a CLGeocoder class which does this for us!

Still in AppDelegate.swift, add this property at the top of the class:

static let geoCoder = CLGeocoder()

Now, at the bottom of locationManager(_:didVisit:), add the following code:

AppDelegate.geoCoder.reverseGeocodeLocation(clLocation) { placemarks, _ in
  if let place = placemarks?.first {
    let description = "\(place)"
    self.newVisitReceived(visit, description: description)
  }
}

Here, you ask geoCoder to get placemarks from the location. The placemarks contain a bunch of useful information about the coordinates, including their addresses. You then create a description string out of the first placemark. Once you have the description string, you call newVisitReceived(_:description:).

Sending Local Notifications

Now, it’s time to notify a user when the new visit location is logged. At the bottom of newVisitReceived(_:description:), add the following:

// 1
let content = UNMutableNotificationContent()
content.title = "New Journal entry 📌"
content.body = location.description
content.sound = .default

// 2
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: location.dateString, content: content, trigger: trigger)

// 3
center.add(request, withCompletionHandler: nil)

With the above, you:

  1. Create notification content.
  2. Create a one second long trigger and notification request with that trigger.
  3. Schedule the notification by adding the request to notification center.

Build and run the app. At this point, the app is usable in that it logs visits and notifies the user.

If you are using a real device and have some time for a walk, you can test your work right now. Go some place and stop to have a coffee. Visits require you remain at a place for some period of time. You should receive some notifications, like this:

The visits are being recorded, but the visits are not yet persisted.

Faking Data (Optional)

Walking is good for your body, but it might be a problem to do it right now in the middle of building this app! To test the app without actually walking, you can use the Route.gpx file. This kind of file allows you to simulate the device or simulator GPS location. This particular file will simulate a walk around Apple’s campus in Cupertino.

To use it, in the Debug area, click the “Simulate Location” icon, and then select “Route” from the list:

You can open the tab with a map or Maps app to see the walking route.

Faking CLVisits

iOS records CLVisits behind the scenes, and sometimes you might wait for up to 30 minutes in order to get the callback! To avoid this, you’ll need to implement mechanics that fake CLVisit recording. You’ll create CLVisit instances, and since CLVisit has no accessible initializer, you’ll need to make a subclass.

Add this to the end of AppDelegate.swift:

final class FakeVisit: CLVisit {
  private let myCoordinates: CLLocationCoordinate2D
  private let myArrivalDate: Date
  private let myDepartureDate: Date

  override var coordinate: CLLocationCoordinate2D {
    return myCoordinates
  }
  
  override var arrivalDate: Date {
    return myArrivalDate
  }
  
  override var departureDate: Date {
    return myDepartureDate
  }
  
  init(coordinates: CLLocationCoordinate2D, arrivalDate: Date, departureDate: Date) {
    myCoordinates = coordinates
    myArrivalDate = arrivalDate
    myDepartureDate = departureDate
    super.init()
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

With this subclass, you can provide initial values for CLVisit‘s properties.

Set Up locationManager

Now you need the locationManager to notify you when the location changes. For this, at the end of application(_:didFinishLaunchingWithOptions:), before return statement, add the following:

// 1
locationManager.distanceFilter = 35

// 2
locationManager.allowsBackgroundLocationUpdates = true

// 3
locationManager.startUpdatingLocation()

Here’s what these lines do:

  1. Receive location updates when location changes for n meters and more.
  2. Allow location tracking in background.
  3. Start listening.

You can comment out these 3 lines to turn off the visits faking.