Home iOS & Swift Tutorials

Location Notifications with UNLocationNotificationTrigger

Learn how to use UNLocationNotificationTrigger to set up location-triggered notifications for your iOS app.

4.8/5 4 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

Geofencing, often called region monitoring, alerts your app when it enters or exits a geographical region. An example of this is an app that alerts users of offers when entering their favorite coffee shop, or an app that alerts the user to check-in on arriving at the dentist. Geofencing in iOS is a powerful feature, and you can use it to drive location-triggered notifications, using UNLocationNotificationTrigger. The feature works when an app is active, in the background, suspended or terminated — all without requiring users to always provide access to their location and sacrifice their privacy.

In this tutorial, you’ll add region monitoring to Swifty TakeOut, which is a simple app that allows users to place an order for pickup.

You’ll enhance the app by alerting users when they arrive using UNLocationNotificationTrigger. Users can then notify the kitchen they are “here” to collect their order.

Along the way, you’ll not only learn what Core Location is, but also how to:

  • Set up Core Location.
  • Register a geofence.
  • Ask the user for permission to use location services.
  • Enable the location update background mode capability.
  • Notify users of a geofence entry event.
  • Register a UNLocationNotificationTrigger.
Note: This tutorial assumes you know the basics of SwiftUI. If you’re new to SwiftUI, check out the SwiftUI: Getting Started tutorial first.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

In the starter project, you’ll find Swifty TakeOut, an app that displays a list of takeout items available for order. Users can later pick up their orders at the Swifty TakeOut location.

Build and run to check out the app:

The Swifty TakeOut menu with an overview of options for ordering

The app allows users to select a food item:

A food item with price and a Place Order button

It also lets them place an order:

An alert showing you placed an order

In Xcode, look at the main files you’ll be working with:

  • TakeOutStore.swift contains a struct representing the takeout store.
  • LocationManager.swift contains an ObservableObject, which is where the magic happens.
  • MenuItem.swift contains a struct representing a menu item and an array of menu items.
  • MenuListView.swift displays a list of items available for order.
  • MenuListRow.swift represents a row item for the menu.
  • DetailView.swift displays the details of the selected item.

In this tutorial, you’ll learn how to show an alert when a user arrives at the Swifty TakeOut restaurant. You’ll do this by registering a geofence and using UNLocationNotificationTrigger set to trigger on that geofence.

But first, you’ll learn a bit more about Core Location.

What Is Core Location?

Core Location is an Apple framework that provides many services to get geographical information from a user’s device. Using the API, you can determine the device’s location, altitude, heading and orientation. Additionally, if any iBeacons (Bluetooth advertisements) are in the vicinity, you can detect and locate them.

The geographic information comes from the device’s hardware components. Where available, that includes Wi-Fi, GPS, Bluetooth, magnetometer, barometer and cellular radio.

Everything revolves around CLLocationManager. You’ll use it to start, stop and configure the delivery of location events in Swifty TakeOut. You can also set its properties to different accuracies and use region monitoring to watch for a user entering or leaving distinct areas.

Note: To learn more about Core Location and its services, check out Apple’s Core Location documentation.

Setting Up Core Location

Before you can detect if a user enters a geographical region, you’ll need to set up Core Location. To do so, you’ll create an instance of CLLocationManager.

Open LocationManager.swift and add the following code under the location property, inside LocationManager:

// 1
lazy var locationManager = makeLocationManager() 
// 2
private func makeLocationManager() -> CLLocationManager {
  // 3
  let manager = CLLocationManager()
  // 4
  return manager
}

Here’s what this code does:

  1. Adds an instance property to store the location manager. A lazy property delays initialization until it’s first used.
  2. Declares a method that creates and configures CLLocationManager.
  3. Creates an instance of the location manager.
  4. Returns the configured CLLocationManager object.

With a location manager configured, you’ll now register your geographical point of interest.

Note: You might be tempted to assign CLLocationManager to the property rather than use the method, but some additional configuration of the CLLocationManager object is needed later.

Creating a Geofence

To create a geofence in iOS, you need to configure a CLCircularRegion. This object consists of a center point and a radius, in meters. An entry event will notify your app when the device moves from outside to inside the radius.

A map showing someone entering a region

An exit event gets triggered when going from inside to outside the radius.

A map showing someone exiting a region

Next, while still in LocationManager.swift, configure your region by adding the following code under makeLocationManager():

// 1
private func makeStoreRegion() -> CLCircularRegion {
  // 2
  let region = CLCircularRegion(
    center: location,
    radius: 2,
    identifier: UUID().uuidString)
  // 3
  region.notifyOnEntry = true
  // 4
  return region
}

Here’s what this code does:

  1. Declares a method that creates a CLCircularRegion.
  2. Creates a CLCircularRegion instance. The center point is location, which is the latitude and longitude of Swifty TakeOut. The radius is two meters, and a unique identifier is associated.
  3. Configures region to trigger a notification when an entry event occurs.
  4. Returns the configured region.

With that added, configure a property to use this returned value. Add the code below the location property:

lazy var storeRegion = makeStoreRegion()

Now that you have set up this utility methods, you need to get the user’s permission to use location information. You’ll do that next.

Placing an Order

The main functionality of Swifty TakeOut is ordering food. When users place a food order, they receive a confirmation alert. This confirmation alert also asks if they’d like to get notified on arrival. Some restaurants offer to bring food out to your car if you select curbside pickup when ordering. When you arrive, you have to tell the restaurant “I’m here!”, so they can bring the food out to your car. In the next few sections, you’ll add this functionality.

Monitoring User Location

For notifications when a region boundary is crossed, you need to start tracking the location of the device. To do this, you’ll need to request authorization from the user.

Back in LocationManager.swift, add the following method under makeStoreRegion():

// 1
func validateLocationAuthorizationStatus() {
  // 2
  switch locationManager.authorizationStatus {
  // 3
  case .notDetermined, .denied, .restricted:
    // 4
    print("Location Services Not Authorized") 
  // 5
  case .authorizedWhenInUse, .authorizedAlways:
    // 6
    print("Location Services Authorized")
  default:
    break
  }
}

Here, you’ve added code that:

  1. Creates a method named validateLocationAuthorizationStatus() that determines the app’s authorization status for using location services.
  2. Configures a switch statement on the current authorization status of the app.
  3. Validates the current authorization value against .notDetermined, .denied or .restricted. If there’s a match, then the code block for this case gets executed.
  4. Prints to the debugger if location services isn’t authorized.
  5. Checks the current authorization value matches .authorizedWhenInUse or .authorizedAlways.
  6. Prints to the debugger if authorized.

You will request to track the user’s location after they place an order. The user’s location is then used to determine if the geofence has been breached.

Next, open DetailView.swift. requestNotification() is called if the user agrees to get notified on arrival. Update this method to confirm the location services authorization status:

func requestNotification() {
  locationManager.validateLocationAuthorizationStatus()
}

With this code added, it’s time to test out what you’ve added.

Build and run:

  1. Select a food item.
  2. Place an order.
  3. Tap Yes to get notified on arrival.

Order Placed Alert

You should see the following printed to the debugger:

Location Services Not Authorized

Next, you’ll request the user’s permission to allow Swifty TakeOut to use location services.

Location Authorization

Your app can request two categories of location authorization:

  1. When in Use: Your app can use location services when it’s running in the foreground. With the help of background modes, the app can also get updates in the background.
  2. Always: Your app can use location services whenever it needs to. If the app isn’t running, the system will wake it up to deliver the event. There’s no need for background modes with this permission.

It’s important to respect your user’s privacy and use When in Use as much as possible. Swifty TakeOut‘s core functionality isn’t location based, so you shouldn’t use the Always permission.

It’s also important to explain to the user why their location needs to get monitored. Many developers prompt for location permission when the user first opens the app. That can be overwhelming for the user since they don’t understand why the app needs their location. Instead, consider asking for user’s permission when they actually want to use the location functionality. For example, your app will request permission when a user taps on Yes to get notified on arrival. The app will also explain in the permission prompt the reason that the app needs the location.

To do this, open Info.plist and add the Privacy – Location When In Use Usage Description key. In raw mode, this is the NSLocationWhenInUseUsageDescription:

Adding NSLocationWhenInUseUsageDescription to Info.plist

For the value, add the string Swifty TakeOut uses your location to check you in on arrival:

Setting the description text

Note: Without the required key-value pair, you’ll be unable to get the user’s location.

Requesting Location Authorization

Now that you’ve added the required key-value pair for location permissions, it’s time to ask the user. Back in LocationManager.swift, inside validateLocationAuthorizationStatus(), locate case .notDetermined, .denied, .restricted. You’ll request the WhenInUse permission here.

Below print("Location Services Not Authorized"), add:

locationManager.requestWhenInUseAuthorization()

With this added, you can now request the user’s permission.

Build and run. You should now see the location authorization dialog the first time an order is placed:

Location authorization dialog

As shown in the screenshot above, there are three categories of responses the user can give:

  1. Allow Once: The user will allow your app to use their location once.
  2. Allow While Using App: The user has given permission to use their location whenever the app is being used. In general, this means the app is in the foreground. However, this permission can include when the app is in the background, suspended or terminated.
  3. Don’t Allow: The user refuses permission.

Tap Allow While Using App.

Triggering a Location Notification on Arrival

You want to alert the user on arrival at Swifty TakeOut. To do this, you’ll create a special type of notification: UNLocationNotificationTrigger. This object creates a local user notification when the device enters or exits a region. Because you configured the geofence region earlier in this tutorial, you can now go ahead and create a location trigger. But before you can do this, you need to ask the user’s permission to receive push notifications.

Notification Authorization

For notifications and configuring a UNLocationNotificationTrigger, you’ll need to use the UserNotifications API.

Open LocationManager.swift and beneath the import for CoreLocation, add:

import UserNotifications

Next, add the following to the top of the LocationManager class with the other properties:

let notificationCenter = UNUserNotificationCenter.current()

Here, you create a reference to the system’s shared notification center. This is the brain for all things notifications in iOS.

Still in LocationManager.swift, add the following method to the bottom of the class:

// 1
func requestNotificationAuthorization() {
  // 2
  let options: UNAuthorizationOptions = [.sound, .alert]
  // 3
  notificationCenter
    .requestAuthorization(options: options) { result, _ in
      // 4
      print("Notification Auth Request result: \(result)")
    }
}

The code above does the following:

  1. Creates a method to request notification authorization.
  2. Defines the notification’s capabilities and requests the ability to play sounds and display alerts.
  3. Requests authorization from the user to show local notifications.
  4. The completion block prints the results of the authorization request to the debugger.

You want to show this to users when they place an order.

Navigate to DetailView.swift and locate requestNotification(). At the bottom of the method, add:

locationManager.requestNotificationAuthorization()

Now, the user will get prompted for notification authorization after placing an order.

Build and run, and then place an order.

The notification authorization step happens each time you place an order. The first time you run a notification authorization in a particular app, you should see an alert requesting permission to send notifications, if they are not enabled by default:

Notification Permissions Prompt

Tap Allow. You’ll see the following printed to the debugger:

Notification Auth Request result: true

Your app is ready to receive notifications. Next, you’ll configure and handle a local notification.

Using UNLocationNotificationTrigger to Notify the User

With permission to receive a push notification added, it’s time to configure one.

Open LocationManager.swift and add the following method to the bottom of the LocationManager class:

// 1
private func registerNotification() {
  // 2
  let notificationContent = UNMutableNotificationContent()
  notificationContent.title = "Welcome to Swifty TakeOut"
  notificationContent.body = "Your order will be ready shortly."
  notificationContent.sound = .default

  // 3
  let trigger = UNLocationNotificationTrigger(
    region: storeRegion,
    repeats: false)

  // 4
  let request = UNNotificationRequest(
    identifier: UUID().uuidString,
    content: notificationContent,
    trigger: trigger)

  // 5
  notificationCenter
    .add(request) { error in
      if error != nil {
        print("Error: \(String(describing: error))")
      }
    }
}

This method does the following:

  1. Creates a method to register a notification.
  2. Creates content for a notification. UNMutableNotificationContent is the object used for representing the content of a notification. Here, you’re setting the title, body and sound properties of this object.
  3. Creates a location trigger condition that causes the notification to appear. This gets triggered when you enter the storeRegion.
  4. Configures a UNNotificationRequest object to request a notification with the notificationContent and trigger. A unique identifier is set in this request. This is useful if you need to cancel a notification.
  5. Adds the request to schedule the notification.

With this added, you need to update requestNotificationAuthorization() to register a notification, if allowed. This requires two modifications. First, change the .requestAuthorization(options: options) { result, _ in line to what’s shown below:

.requestAuthorization(options: options) { [weak self] result, _ in

You’re adding [weak self] to the capture list for the completion block. That allows you to access the properties of the class from inside the completion block.

Next, after print("Notification Auth Request result: \(result)"), add:

if result {
  self?.registerNotification()
}

This code tests the authorization result to see if the user has given permission to receive notifications. If so, register the region trigger notification.

Now, users should get notified when they enter the region.

Handling Notification Action

You’re almost there! To handle notifications and their actions, you need to add UNUserNotificationCenterDelegate.

Open LocationManager.swift, and add the following extension after the LocationManager class:

extension LocationManager: UNUserNotificationCenterDelegate {}

This extension allows LocationManager to act as the UNUserNotificationCenter‘s delegate.

With the delegate set up, add the following code inside LocationManager, beneath the properties:

// 1
override init() {
  super.init()
  // 2
  notificationCenter.delegate = self
}

This code:

  1. Overrides the initializer of the NSObject to provide custom initializer code.
  2. Configures LocationManager to be a delegate of notificationCenter.

Next, add these delegate methods inside the extension:

// 1
func userNotificationCenter(
  _ center: UNUserNotificationCenter,
  didReceive response: UNNotificationResponse,
  withCompletionHandler completionHandler: @escaping () -> Void
) {
  // 2
  print("Received Notification")
  // 3
  completionHandler()
}

// 4
func userNotificationCenter(
  _ center: UNUserNotificationCenter,
  willPresent notification: UNNotification,
  withCompletionHandler completionHandler:
    @escaping (UNNotificationPresentationOptions) -> Void
) {
  // 5
  print("Received Notification in Foreground")
  // 6
  completionHandler(.sound)
}

Here’s an overview of the code:

  1. This delegate method will be called if the user taps on the notification.
  2. Prints to the debugger when you receive a notification.
  3. Tells the system you finished handling the notification.
  4. This delegate method handles a notification that arrives when the app is in the foreground.
  5. Prints to the debugger when you receive a notification in the foreground.
  6. Tells the system that you’ve finished handling the foreground notification and it should only play a notification sound, not display the notification.

With this code added, you can now test out your location notifications.

Simulating User Movement

Unless you’re lucky enough to live near Apple Campus, where the fictional Swifty TakeOut is, you’ll need to simulate entering the region. To do this, you’ll need to add a .gpx file. GPX, or GPS Exchange Format, is an XML file used to describe waypoints, tracks and routes:

  1. Go to the Project navigator.
  2. Right-click the Swifty TakeOut group.
  3. Click New File….
  4. Select GPX File.
  5. Name it SimulatedLocations.
  6. Click Create.

With SimulatedLocations.gpx added, replace its contents with the following:

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
  <wpt lat="37.422155" lon="-122.134751">
    <name>Apple Park</name>
    <time>2021-01-23T14:00:00Z</time>
  </wpt>
  <wpt lat="37.33182000" lon="-122.03118000">
    <name>Apple Campus</name>
    <time>2021-01-23T14:00:05Z</time>
  </wpt>
</gpx>

Here, you’ve added an XML file that contains two waypoints: Apple Park and Apple Campus. You’ll notice that each waypoint has a timestamp. They’re five seconds apart, simulating a five-second journey from Apple Park to Apple Campus.

With the GPX file added, you can start simulating an entry event.

Connecting to a physical device

Unfortunately, simulating UNLocationNotificationTrigger doesn’t appear to work well in the simulator. But, it does work on a physical device hooked up to Xcode. To follow the steps in this section, you’ll need to connect an iOS device to your computer with a USB cable.

On the connected device, you’ll see a notification asking if you want to trust the computer:

Screenshot Trust This Computer Notification

Tap Trust.

Next you’ll need to set the developer team for your project:

  1. In Xcode, select the project from the Project navigator.
  2. Click the SwiftyTakeOut app target.
  3. Select the Signing & Capabilities tab.
  4. Select your developer account from the Team drop-down control in the Signing section.

Screenshot Xcode Developer Team

You’re ready to install the app on your device. Use the Scheme menu to set the active scheme to your connected device:

Screenshot Xcode active scheme

Build and run.

Xcode will install the app on your device and try to open it. When Xcode tries to open the app, it will probably show an error due to security restrictions:

Screenshot Xcode could not launch app

To fix the error, open the Settings app on your physical iOS device. Navigate to General ▸ Device Management ▸ Apple Development: [account]:

Screenshot Settings Apple Developer Trust

Tap Trust “Apple Development: [account]”.

A notification will appear. Tap Trust to confirm.

Once your developer account has been trusted and your app has been verified, Xcode is ready to be able to launch the app.

With your iOS device unlocked, build and run from Xcode. The app should open on the physical device:

The Swifty TakeOut menu with an overview of options for ordering

Woohoo! You’re ready to proceed with the rest of this tutorial.

Simulating the Journey

Now you’re ready to try the simulated locations in the GPX file you added.

Build and run on your connected iOS device. When the app launches, select an item from the menu, place an order and tap Yes to get notified:

App screenshot notification

Notifications will appear asking permission: Tap Allow While Using App for location and Allow for notifications.

Next, switch back to Xcode, select the Location icon in the Debug bar and choose SimulatedLocations:

Selecting a custom location

Note: Sometimes the Location icon doesn’t appear on the Debug bar. If this happens, stop debugging and build and run the app again.

Your simulated journey will now start. After five seconds or so have passed, you’ll see the following in the debugger:

Arrived at TakeOut - Foreground

The output gets printed when your app is in the foreground and userNotificationCenter(_:willPresent:withCompletionHandler:) in LocationManager.swift is called.

Next, either stop the app in Xcode and build and run the app again, or stop simulating the location. Now place a new order and tap Yes to get notified. Then, before simulating the user journey, put the app in the background by either locking the screen or going to the Home screen. Once the app is in the background, simulate your journey. You’ll now see a local notification appear:

Local Notification

If you tap the notification, it’ll launch the app and print the following to the debugger:

Received Notification

The difference here is that if your app is in the foreground, you won’t see the notification, but if it’s in the background, then you’ll see it.

Congratulations, your app now notifies you when you enter the region! But your journey isn’t over yet!

Blue-haired person in the driver seat of a car, singing, while a bird flies outside

Printing to the debugger on arrival is cool, but wouldn’t it be great if your user is also alerted?!

In the next section you’ll improve the user experience by:

  1. Showing an alert when the app is in the foreground.
  2. Showing an alert when the app gets launched by tapping a notification.

Handling a Geofence Entry Event

In this section, you’ll show an alert to the user if the app is in the foreground when the geofence is entered. Finally, a local notification will wake the app when in the background.

Alerting the Kitchen

Open LocationManager.swift and add the following beneath storeRegion at the top of the class:

@Published var didArriveAtTakeout = false

When didArriveAtTakeout changes, then all the views referencing it get notified and can execute code in response. In your app, when this value changes to true, the user will see an alert.

Next, still in LocationManager.swift, update both userNotificationCenter(_:didReceive:withCompletionHandler:) and userNotificationCenter(_:willPresent:withCompletionHandler:) with the following code, added above the completionHandler() call in each method:

didArriveAtTakeout = true

Here, you’re updating the value because you’ve arrived at your destination.

Now, you’re going to show an alert when this value changes. To do this, open ContentView.swift and add an alert modifier in body after .navigationViewStyle(StackNavigationViewStyle()):

.alert(isPresented: $locationManager.didArriveAtTakeout) {
  Alert(
    title: Text("Check In"),
    message:
      Text("""
        You have arrived to collect your order.
        Do you want to check in?
        """),
    primaryButton: .default(Text("Yes")),
    secondaryButton: .default(Text("No"))
  )
}

This alert gets executed any time didArriveAtTakeout changes to `true`.

Build and run the app on your connected iOS device and test this out:

  1. Place an order.
  2. Simulate the locations using the debugger.

Now when you arrive at Swifty TakeOut, the app will ask the user to check in:

Notify the kitchen alert

Next, try putting the app into the background after placing an order, then simulate locations.

You’ll first see a banner notification that says your food will be ready shortly. When you tap that banner, the app will launch. You’ll then see the alert that allows you to Check In.

Wow! Congrats! Your app has come a long way. Users of your app are now notified when they arrive to collect their order, allowing them to get curbside pickup.

But wait, there’s a small issue you have to resolve: If your app is in a suspended or terminated state, your user won’t be notified to check in. In this next section, you’ll fix this issue.

Getting Background Updates

To fix this, changing the permission to Always isn’t the answer, for privacy reasons mentioned in an earlier section. Instead, enter Background Modes, which allows you to continue to use the When in Use authorization.

To enable this:

  1. Select the project from the Project navigator.
  2. Click the SwiftyTakeOut app target.
  3. Select the Signing & Capabilities tab.
  4. Click the + Capability tab.
  5. Select Background Modes.
  6. Under the Background Modes section, enable Location updates.

Background Modes - Location Updates

Next, open LocationManager.swift and add the following code in makeLocationManager() above the return manager statement:

manager.allowsBackgroundLocationUpdates = true

This code allows your app to receive location updates when it’s in a suspended state. This state occurs when the app is in the background for some time and no longer able to execute code. With this enabled, your app gets notified when the device enters the geofence, waking it up to handle any events.

Local Notification

Note: Unfortunately, you can’t test your app from a terminated state in Xcode. To test this, you need to:
  1. Pick a nearby location as your test region. Apple Maps or Google Maps will give you the longitude and latitude you need for the location property.
  2. Build and run the app on your connected iOS device.
  3. Stop the app in Xcode and then disconnect the USB cable from device.
  4. Walk at least 200 meters away from that location.
  5. Open the app and place an order.
  6. Quit the app by launching the App Switcher and swiping upwards.
  7. Walk toward your chosen location. You should then get the notification as you get close to the location.

It means going outside, but on the plus side, you’ll be a couple a steps closer to your daily goal. :]

That’s a wrap!

A Happy Monster to celebrate completing the tutorial

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Congratulations! You’ve completed this tutorial and can successfully notify users when they arrive at Swifty TakeOut.

In this tutorial, you learned how to:

  1. Set up and use Core Location.
  2. Register a geofence for region monitoring.
  3. Enable your app for location updates when the app is in the background.
  4. Notify users of a geofence entry event.
  5. Request user location permissions and set the message text.

In addition, you learned why you shouldn’t ask for the Always location permission if you don’t need it.

If you’re interested in learning more about geofencing, read more by checking out our Geofencing with Core Location tutorial and Apple’s documentation on geofencing.

Finally, if you’d like to learn more about handling location updates in the background, check out Apple’s Handling Location Events in the Background article.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

4.8/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments