Geofencing Tutorial for iOS

A geofence is a virtual fence or a perimeter around a physical location. When an object enters this area, something happens. Easy way to think about it a fence around your home. When someone enters your yard, the burglar alarm is activated.

Geofencing notifies your app when its device enters or leaves geographical regions you set up. It lets you make cool apps that can trigger a notification whenever you leave home, or greet users with the latest and greatest deals whenever favorite shops are nearby.

There is a limitation: As geofences are a shared system resource, Core Location restricts the number of registered geofences to a maximum of 20 per app.

Before any geofence monitoring can happen, though, you need to set up a CLLocationManager instance and request the appropriate permissions.

let locationManager = CLLocationManager()

override func viewDidLoad() {
    super.viewDidLoad()
    locationManager.delegate = self
    locationManager.requestAlwaysAuthorization()
}

You call requestAlwaysAuthorization(), which displays a prompt to the user requesting authorization to use location services Always. Apps with geofencing capabilities require Always authorization since they must monitor geofences even when the app isn’t running. Info.plist already contains the message to show the user under the key NSLocationAlwaysAndWhenInUseUsageDescription. Since iOS 11, all apps that request Always also allow the user to select When In Use. Info.plist also contains a message for NSLocationWhenInUseUsageDescription. It’s important to explain to your users as simply as possible why they need to have Always selected.

With the location manager properly configured, you must now allow your app to register user geofences for monitoring. Core Location requires you to represent each geofence as a CLCircularRegion instance.

let someCoordinate = CLLocationCoordinate2DMake(37.33233141, -122.0312186)

let region = CLCircularRegion(center: someCoordinate, 
    radius: min(someRadius, locationManager.maximumRegionMonitoringDistance), 
    identifier: someIdentifier)

region.notifyOnEntry = true
region.notifyOnExit = true

CLCircularRegion also has two boolean properties: notifyOnEntry and notifyOnExit. These flags specify whether to trigger geofence events when the device enters or leaves the defined geofence, respectively.

Next, you need a method to start monitoring a given geotification whenever the user adds one.

func startMonitoring(region: CLCircularRegion) {
    if !CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
        showAlert(withTitle:"Error", message: "Geofencing is not supported on this device!")
        return
    }

    if CLLocationManager.authorizationStatus() != .authorizedAlways {
        let message = """
            Your geotification is saved but will only be activated once you grant 
            Always permission to access the device location.
        """
        showAlert(withTitle:"Warning", message: message)
    }

    locationManager.startMonitoring(for: region)
}

With your start method done, you also need a method to stop monitoring a given geotification when the user removes it from the app.

locationManager.stopMonitoring(for: region)

Reacting to Geofence Events

Open AppDelegate.swift. This is where you'll add code to properly listen for and react to geofence entry and exit events. Add the following line at the top of the file to import the CoreLocation framework:

import CoreLocation

Add a new property below var window: UIWindow?:

let locationManager = CLLocationManager()

Replace application(_:didFinishLaunchingWithOptions:) with the following implementation:

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

Add the following method to AppDelegate.swift:

func handleEvent(for region: CLRegion!) {
    print("Geofence triggered!")
}

Next, add the following extension at the bottom of AppDelegate.swift:

extension AppDelegate: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        if region is CLCircularRegion {
            handleEvent(for: region)
        }
    }

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

    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)")
    }

}

Notifying the User of Geofence Events

In AppDelegate.swift, add the following import:

import UserNotifications

Add the following statements to the end of application(_:didFinishLaunchingWithOptions:), just before the method returns:

let options: UNAuthorizationOptions = [.badge, .sound, .alert]
UNUserNotificationCenter.current()
    .requestAuthorization(options: options) { success, error in
        if let error = error {
            print("Error: \(error)")
        }
}

Finally, add the following method:

func applicationDidBecomeActive(_ application: UIApplication) {
    application.applicationIconBadgeNumber = 0
    UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
    UNUserNotificationCenter.current().removeAllDeliveredNotifications()
}

Next, replace handleEvent(for:) with the following:

func handleEvent(for region: CLRegion!) {
    // Show an alert if application is active
    if UIApplication.shared.applicationState == .active {
        guard let message = note(from: region.identifier) else { return }
        window?.rootViewController?.showAlert(withTitle: nil, message: message)
    } else {
        // Otherwise present a local notification
        let notificationContent = UNMutableNotificationContent()
        notificationContent.body = "Geofence triggered!"
        notificationContent.sound = UNNotificationSound.default()
        notificationContent.badge = UIApplication.shared.applicationIconBadgeNumber + 1 as NSNumber
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
        let request = UNNotificationRequest(identifier: "geofenceId",
                                content: notificationContent,
                                trigger: trigger)
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Error: \(error)")
            }
        }
    }
}