• Geofencing Tutorial with Core Location
  • Originally by Andy Pereira
  • Translator: yrq110

Update: This tutorial has been updated to Xcode 8 / Swift 3.

Create a fence!

Geo-fencing will send a notification to the app to alert you when the device enters or leaves the area you have set. This lets you do cool things like set up a notification when you leave home or greet users with the latest and greatest items at their favorite store nearby. In this geo-fencing tutorial, learn how to use Swift for area Monitoring in iOS – using the Region Monitoring API in Core Location.

In this tutorial, you’ll create a location alert application called Geotify that lets users create reminders associated with real world locations. Let’s go!

An introduction to

Download to start the project. The project offers a simple feature: it allows users to add or remove points on a map, and each point represents a location alert, or geonotification.

Build and run, and you’ll see an empty Map View like this.

Click the + button in the navigation bar to add a new geographic notification. A separate view appears in the app to set the various properties of the geographic notification.

In this tutorial you need to add a pin to the Apple headquarters in Cupertino. If you don’t know where it is, go to Google Maps to find the point, remember to use zoom to improve accuracy!

Note: In the simulator, kneading gesture is used for zooming. Hold down the Option key and press Shift to move the kneading center, then release shift, click and drag to kneading operation.

Radius indicates the distance from the specified location. If this distance is exceeded, an iOS notification will be triggered. Note indicates the information displayed in the notification. The app also allows users to use a segmented control at the top to set when a notification is triggered to enter or leave the circular geofencing.

Type 1000 in RADIUS and Say Hi to Tim in note! Select Upon Entry at the top so that the first geographic notification is set up.

Click Add and you’ll see a new marker pin appear in the Map View, which is the geographic notification you just added, with a circle around the marker to represent the defined geographic fence:

Clicking on the pin displays details about the geographic notification, such as the content of the alert and the event type specified previously. Don’t click on that cross unless you want to delete it!

Feel free to add or remove any geographic notifications. Because the app uses NSUserDefaults to store persistent data, the previous list of geographic notifications is retained when the app is restarted.

Set up the LocationManager and permissions

At this point, any geographic notifications you add to the Map View are simply displayed and have no actual detection effect. To enable fence detection, you need to register each fence associated with a geographic notification on Core Location.

Before enabling fence monitoring, you need to set up a LocationManager instance and request some permissions.

Open GeotificationsViewController. Swift, on top of the class, a constant CLLocationManager instance declaration is as follows:

class GeotificationsViewController: UIViewController {

  @IBOutlet weak var mapView: MKMapView!

  var geotifications = [Geotification]()
  let locationManager = CLLocationManager() // Add this statement

  ...
}Copy the code

Then replace the viewDidLoad() method with the following code:

override func viewDidLoad() {
  super.viewDidLoad()
  // 1
  locationManager.delegate = self
  // 2
  locationManager.requestAlwaysAuthorization()
  // 3
  loadAllGeotifications()
}Copy the code

To implement this method step by step:

  1. Set the delegate of the locationManager instance to the current view controller.
  2. Call requestAlwaysAuthorization () method, prompt the user application requests a authorization use location services. Always authorization is required for the geo-fencing function of the APP, because the fence detection is performed even when the app is not running in the foreground. The Info. In the plist NSLocationAlwaysUsageDescription keys are set up when the request information displayed when the user’s location.
  3. Call loadAllGeotifications() to deserialize the list of geographic notifications previously saved in NSUserDefaults and store it in the local geographic notifications array. This method also loads geographic notifications labeled on the Map View.

When the app hint user authorization, displays the values in the NSLocationAlwaysUsageDescription, friendly why app need access to the user’s location. This key is required to request location service authorization. Without this key, the system will ignore the request and refuse to provide location service.

Build and run, and you’ll see a user prompt that displays the information you set earlier:

You’ve finished setting up the app to request permission. Good job! Click Allow to ensure that the Location Manager receives callbacks from the delegate method at the appropriate time.

Before implementing geofencing, there is one small problem to solve: the current user location is not yet displayed on the Map View! This feature is not yet enabled, so the zoom button on the left of the navigation bar does not work.

Fortunately, this issue is not a big deal. You need to enable the map View to display the current location after the app is authorized.

Enters GeotificationsViewController. Swift file, add the following to entrust in the CLLocationManagerDelegate expansion method, :

extension GeotificationsViewController: CLLocationManagerDelegate {
  func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    mapView.showsUserLocation = (status == .authorizedAlways)
  }
}Copy the code

When authorized state changes, the location manager will call locationManager (_ : didChangeAuthorizationStatus:) method. If the user has given the app permission to use the location service, this method is called after initializing the Location Manager and setting its delegate.

This method is in place to check if the app is authorized, and if so, to display the user’s current location in the Map View.

Build and run the app, and if it’s running on a real machine, you’ll see a location marker appear in the main Map View. If running on an emulator, click Debug\Location\Apple in the menu to display the Location mark:

In addition, the zoom button on the navigation bar now works. :]

Registered Geofencing

After setting up the Location Manager normally, the next thing to do is to register the user’s geo-fencing in the app.

In the APP, users’ geo-fencing information is stored using a custom geo-notification model. However, Core Location requires that each geographic fence must be an instance of CLCircularRegion prior to registration. To solve this problem, you need to create a help method to convert the geographic notification object to CLCircularRegion.

Open GeotificationsViewController. Swift file, add the following methods:

func region(withGeotification geotification: Geotification) -> CLCircularRegion { // 1 let region = CLCircularRegion(center: geotification.coordinate, radius: geotification.radius, identifier: geotification.identifier) // 2 region.notifyOnEntry = (geotification.eventType == .onEntry) region.notifyOnExit = ! region.notifyOnEntry return region }Copy the code

What is done in the code above:

  1. First, a CLCircularRegion was initialized using the location and radius of the geo-fencing and a recognizer (in order for iOS to distinguish between the fencing registered in the app). Initialization is simple and the Geotification model already contains the required attributes.
  2. CLCircularRegion instances have two Boolean properties: notifyOnEntry and notifyOnExit. These markers specify whether a fence event is triggered when a device enters or leaves the designated fence. If your app is designed to allow only one notification per fence, set one of the identifiers to true and the other to false based on the enumerated values saved in the Geotification object.

Next, you need a way to start monitoring when a user adds geographic notifications.

In GeotificationsViewController add the following methods:

func startMonitoring(geotification: Geotification) { // 1 if ! CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) { showAlert(withTitle:"Error", message: "Geofencing is not supported on this device!" ) return } // 2 if CLLocationManager.authorizationStatus() ! = .authorizedAlways { showAlert(withTitle:"Warning", message: "Your geotification is saved but will only be activated once you grant Geotify permission to access the device location.") } // 3 let region = self.region(withGeotification: geotification) // 4 locationManager.startMonitoring(for: region) }Copy the code

Step by step:

  1. IsMonitoringAvailableForClass (_) indicates whether the device contains a fenced in detection of hardware required. Display an appropriate warning if the monitoring function is not available. ShowSimpleAlertWithTitle (_: Message :viewController) is a utilities. swift helper function that receives title and message data and pops up an alert view.
  2. Next, you need to check the authorization status to make sure your app has the permissions it needs to use location services. If the app is not authorized, it cannot receive any notification related to the fence. However, even if the app is not authorized, you should also allow the user to save geographical notifications. Core Location allows you to sign up for a fence. The monitoring function of these fences will be automatically enabled when the user authorizes the app later.
  3. Create an instance of CLCircularRegion using the input geographic notification and the previously defined help methods.
  4. Finally, CLCircularRegion instances are registered with Core Location for monitoring.

Once the method of enabling monitoring is worked out, you also need a way to stop monitoring when the user removes geographic notifications.

In GeotificationsViewController. Swift file, in startMonitoringGeotificiation (_) below to add the following method:

func stopMonitoring(geotification: Geotification) {
  for region in locationManager.monitoredRegions {
    guard let circularRegion = region as? CLCircularRegion, circularRegion.identifier == geotification.identifier else { continue }
    locationManager.stopMonitoring(for: circularRegion)
  }
}Copy the code

This method commands the locationManager to stop monitoring the CLCircularRegion associated with the incoming geographic notification.

Ok, now that you’ve completed the start and stop methods that will be used to add or remove geographic notifications, it’s time to develop the add notifications section.

First, look at the GeotificationsViewController. Swift document addGeotificationViewController (_ : didAddCoordinate) method.

This method is entrusted by AddGeotificationViewController call method to create the geographical notice, this method is responsible for use from AddGeotificationsViewController values to create a new Geotification object, And update the corresponding Map View and geographic notification list. Then call saveAllGeotifications() to update the geographic notification list and save it to NSUserDefaults.

Replace this method with the following code:

func addGeotificationViewController(controller: AddGeotificationViewController, didAddCoordinate coordinate: CLLocationCoordinate2D, radius: Double, identifier: String, note: String, eventType: EventType) {
  controller.dismiss(animated: true, completion: nil)
  // 1
  let clampedRadius = min(radius, locationManager.maximumRegionMonitoringDistance)
  let geotification = Geotification(coordinate: coordinate, radius: clampedRadius, identifier: identifier, note: note, eventType: eventType)
  add(geotification: geotification)
  // 2
  startMonitoring(geotification: geotification)
  saveAllGeotifications()
}Copy the code

Two key points in the code:

  1. To ensure a uniform radius value with locationManager maximumRegionMonitoringDistance attribute, this property is defined as the maximum radius of geographical fence. This is important, as any value above this maximum will cause monitoring to fail.
  2. Call startMonitoringGeotification (_), the use of the Core Location register associated with the newly added geographic circular monitoring the fence.

It is now possible to register new monitoring fences in the app, but there is a limitation: Geo-fences are a shared system resource, and Core Location limits the number of registered fences to a maximum of 20 per app.

While there are workarounds to this limit, for the purposes of this tutorial, limit the number of geographical notifications a user adds.

As follows, in updateGeotificationsCount () method to add one line of code:

func updateGeotificationsCount() { title = "Geotifications (\(geotifications.count))" navigationItem.rightBarButtonItem? .isEnabled = (geotification.count < 20) // Add this line}Copy the code

Disable the Add button in the navigation bar when the number of notifications in the app reaches a critical value.

Finally, to deal with the geographical notice remove operation, the operation in the mapView (_ : annotationView: calloutAccessoryControlTapped:) method, when users click on each mark point attached call this method when the delete button.

In the mapView (_ : annotationView: call stopMonitoring calloutAccessoryControlTapped:) method add (geotification:) code:

func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {// Delete geonotification let geonotification = view.annotation as! Geotification stopMonitoring(geotification: RemoveGeotification (geotification) saveAllGeotifications()}Copy the code

The added statement stops monitoring the fence associated with geographic notifications, then removes the notifications and saves the changes to NSUserDefaults.

Ok, now the app can monitor and unmonitor users’ geo-fencing, come on!

Build and run the project, and the app can now register a monitored geo-fence, although you can’t see any noticeable changes. However, it can’t respond to any fence incidents yet, don’t worry, this is the next mission!

Add geo-fencing in response to events

It’s important to implement some delegate methods that handle errors first, in case something goes wrong and you don’t know why.

Enters GeotificationsViewController. Swift file, add the following method: in the expansion of the CLLocationManagerDelegate

func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion? , withError error: Error) { print("Monitoring failed for region with identifier: \(region! .identifier)") } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("Location Manager failed with the following error: \(error)") }Copy the code

These delegate methods simply log errors encountered by Location Manager for easy debugging.

Note: You’ll want your app to handle these errors more efficiently in production, rather than silently, letting users know what’s wrong, for example.

Next, open Appdelegate. swift and add code here to listen for and respond to geofencing and exit events.

Add the following code to the CoreLocation framework in the header of the file:

import CoreLocationCopy the code

Make sure the AppDelegate contains an instance of CLLocationManager:

class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  let locationManager = CLLocationManager() // Add this statement
  ...
}Copy the code

Use the following code to replace the application (_ : didFinishLaunchingWithOptions:) :

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
  locationManager.delegate = self
  locationManager.requestAlwaysAuthorization()
  return true
}Copy the code

This sets up receiving fencing-related events in the AppDelegate, but you might be wondering “Why don’t I let the View Controller do this instead of the AppDelegate?”

The fences registered in the app are always monitored, even when the app is not running. If the device triggers a fence event when the app is not running, iOS will automatically restart the app from the background. This makes the AppDelegate an ideal entry point for handling events, since the View Controller may not be loaded or ready.

You might also be wondering “How does a new instance of CLLocationManager know about the fences being monitored?”

In fact, monitoring fences registered in all apps can easily be accessed by all Location Managers, so the initial location of Location Managers does not matter. It’s great, isn’t it? :]

Now all that remains is to implement the relevant delegate methods in response to the fenced event. Before you can do that, you need to add a function that handles the fenced event.

Add the following methods to appdelegate. swift:

func handleEvent(forRegion region: CLRegion!) {
  print("Geofence triggered!")
}Copy the code

This method takes a CLRegion object and simply outputs a text message. Don’t worry – the details will be implemented later.

Next, the AppDelegate. Swift the CLLocationManagerDelegate extension method to add the following to entrust under, and add a newly created handleRegionEvent (_) function calls:

extension AppDelegate: CLLocationManagerDelegate {

  func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    if region is CLCircularRegion {
      handleEvent(forRegion: region)
    }
  }

  func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
    if region is CLCircularRegion {
      handleEvent(forRegion: region)
    }
  }
}Copy the code

It is recommended to name methods with appropriate names. Start locationManager(_:didEnterRegion:) when the device enters a CLRegion and start locationManager(_:didExitRegion:) when the device leaves the CLRegion.

All of these methods return CLRegion. You need to check and make sure it is a CLCircularRegion object, because it could be a CLBeaconRegion object if the app is monitoring the iBeacon region. If it is indeed a CLCircularRegion object, handleRegionEvent(_:) is called.

Note: Fencing events are only triggered when iOS devices cross borders. IOS does not generate an event if the user is already in the registered fence. Apple provides a requestStateForRegion(_:) method to query whether the device location is inside or outside the given fence.

Now that the app can receive fenced events, it’s time to test them out. :]

The most accurate way to test your app is to deploy it to your device, add some geolocation notifications, and then go for a walk or drive test.

It’s probably best not to do that right now, as there’s no way to confirm the log output when the device fence event is triggered in production, and it would be nice to make sure the app works before actual testing.

Fortunately, there is an easy way to do this without leaving the comfort of your home, and Xcode allows you to use a hard-coded GPX file to simulate test locations in your project. This file was included in the start project for convenience. :]

In the Supporting Files folder, locate and open TestLocations. GPX, select inspect for its contents, and you’ll see the code shown below:

The XML version = "1.0"? > Google AppleCopy the code

The GPX file is actually an XML file containing two base points: Google headquarters in Mountain View and Apple headquarters in Cupertino.

Build and run the project to start simulating the location in the GPX file. When your app starts the main View Controller, go back to Xcode, select the Location icon in the Debug toolbar, and then select TestLocations:

Go back to the app and use the Zoom button on the upper left of the navigation bar to find the current location. As you approach the area, you’ll see a location marker moving between Google and Apple headquarters.

Test the app by adding some geographic notifications on the base path. If you added notifications before implementing registered geographic notifications, they are useless and need to be cleaned up and restarted.

For the location of the test, it is a good idea to place geographic notifications at each reference point, using the following test scheme:

  • Google: Radius: 1000m, message: “Say Bye to Google! , notification of departure
  • Apple: Radius: 1000m, information: “Say Hi to Apple!” , notification upon entry

When notifications are added, a log is printed to the console every time a location marker enters or leaves a fence. If you press the home button or lock the screen to place the app in the background, the device will also see the log every time it passes through the fence, although it will not be able to see and confirm the movement on the screen.

Note: Location emulation can be used in iOS emulators as well as real machines. However, in the iOS emulator, the accuracy is lower, and the timing of the triggering event does not quite coincide with the moment when the simulated position is seen entering and exiting the fence. Better use your own real machine to simulate the location, or just go out and try it!

Add notification of fencing events

Now that you’ve completed most of the app, all that’s left to do is implement a feature that alerts users when the device passes through a geofencing fence.

To retrieve the records associated with the triggered CLCircularRegion, you need to retrieve the corresponding geographic notifications in NSUserDefaults. This is not difficult, just use the identifier assigned to CLCircularRegion at registration time to find the correct geographic notification.

Go to AppDelegate.swift and add the following helper methods at the bottom of the class:

func note(fromRegionIdentifier identifier: String) -> String? { let savedItems = UserDefaults.standard.array(forKey: PreferencesKeys.savedItems) as? [NSData] let geotifications = savedItems? .map { NSKeyedUnarchiver.unarchiveObject(with: $0 as Data) as? Geotification } let index = geotifications? .index { $0? .identifier == identifier } return index ! = nil ? geotifications? [index!] ? .note : nil }Copy the code

This helper method retrieves and returns geographic notifications from persistent data based on identifiers.

Now that you can retrieve the records associated with the fence, you need to write another piece of code that sends a notification when the fence event is started, using the record as the information to display.

In the application (_ : didFinishLaunchingWithOptions:) method to the bottom of the return statement before adding the following code:

application.registerUserNotificationSettings(UIUserNotificationSettings(types: [.sound, .alert, .badge], categories: nil))
UIApplication.shared.cancelAllLocalNotifications()Copy the code

The added code allows the user to give the app permission to send notifications, and does a bit of cleanup to clear up existing notifications.

Next, replace the handleRegionEvent(_:) with the following code:

func handleEvent(forRegion region: CLRegion!) {/ / if the app is running the show an alert box if UIApplication. Shared. ApplicationState = =. The active {guard let the message = note(fromRegionIdentifier: region.identifier) else { return } window? .rootViewController? .showAlert(withTitle: nil, message: Message)} else {// otherwise display a local notification let notification = UILocalNotification() notification.alertBody = note(fromRegionIdentifier: region.identifier) notification.soundName = "Default" UIApplication.shared.presentLocalNotificationNow(notification) } }Copy the code

If the app is running, an alert view with logged information is displayed, otherwise a local notification with the same information is sent.

Build and run the project, and when a fence event is triggered during a test, you see an alert view that displays a reminder message:

During the test, press the Home button or lock the screen to put the APP in the background, and you can still receive notification of fence events periodically:

At this point, you have completed a fully functional location alert app by hand. Ok, go out and try the app!