Home iOS & Swift Books Push Notifications by Tutorials

13
Local Notifications Written by Scott Grosch

Although you’ve put together the key concepts up to this point, there is one more category of notifications to cover: local notifications.

While the vast majority of notifications displayed on your device are remote notifications, it’s also possible to display notifications originating from the user’s device, locally. There are three distinct types of local notifications:

  1. Calendar: Notification occurs on a specific date.
  2. Interval: Notification occurs after a specific amount of time.
  3. Location: Notification occurs when entering a specific area.

While less frequently used, local notifications still play an important role for many apps. You should also challenge the immediate notion of using a remote notification. For example, if you provide a food-ordering app, it might want to tell the user that the food is ready to pick up. Will the restaurant really take action when the food is ready or could you, instead, use an interval-based local notification to send the alert after a 10-minute waiting period?

You still need permission!

Even though the notification is created and delivered locally, on the user’s device, you must still obtain permission to display local notifications. Just like remote notifications, the user can grant or remove permissions at any time.

The only difference when requesting permissions locally is that you do not call the registerForRemoteNotifications method on success:

func registerForLocalNotifications(application: UIApplication) {
  let center = UNUserNotificationCenter.current()
  center.requestAuthorization(
    options: [.badge, .sound, .alert]) {
    [weak center, weak self] granted, _ in
    guard granted, let center = center, let self = self 
    else { return }

    // Take action here
  }
}

Note: Since the user may revoke permissions at any time, view controllers creating a local notification must check for permission in viewDidAppear.

Objects versus payloads

The primary difference between remote and local notifications is how they are triggered. You’ve seen that remote notifications require some type of external service to send a JSON payload through APNs. Local notifications use all the same type of data that you provide in a JSON payload but they instead use class objects to define what is delivered to the user.

Creating a trigger

Local notifications utilize what is referred to as a trigger, which is the condition under which the notification will be delivered to the user. There are three possible triggers, each corresponding to one of the notification types:

UNCalendarNotificationTrigger

Not surprisingly, this trigger occurs at specific points in time. While you might assume that you’d be using a Date to specify when the trigger goes off, you’ll actually use DateComponents. A Date distinctly specifies one specific point in time, which isn’t always helpful for a trigger.

let components = DateComponents(hour: 8, minute: 30, weekday: 2)
let trigger = UNCalendarNotificationTrigger(dateMatching: components,
                                            repeats: true)

UNTimeIntervalNotificationTrigger

This trigger is perfect for timers. You might want to display a notification after 10 minutes, rather than at a specific time. You just tell iOS how many seconds in the future the notification should be delivered. If you need the trigger to happen at a specific time, like 2 p.m., you should be using the UNCalendarNotificationTrigger instead to avoid numerous time zone issues related to dates.

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10 * 60,
                                                repeats: false)

UNLocationNotificationTrigger

If you’re a fan of geocaching, this one’s for you! Utilizing this trigger allows you to specify a CLCircularRegion that you wish to monitor. When the device enters said area, the notification will fire. You need to know the latitude and longitude of the center of your target location as well as the radius that should be used. Those three items define a circular region on the map, which iOS will monitor for entry.

let oneMile = Measurement(value: 1, unit: UnitLength.miles)
let radius = oneMile.converted(to: .meters).value
let coordinate = CLLocationCoordinate2D(latitude: 37.33182,
                                        longitude: -122.03118)
let region = CLCircularRegion(center: coordinate, 
                              radius: radius,
                              identifier: UUID().uuidString)

region.notifyOnExit = false
region.notifyOnEntry = true

let trigger = UNLocationNotificationTrigger(region: region,
                                            repeats: false)

Defining content

Excellent; you now know when the trigger is going to go off. It’s time to tell iOS what should be presented in the notification. This is where the UNMutableNotificationContent class comes into play. Be sure to note the “Mutable” in that class’s name. There’s also a class called UNNotificationContent, which you won’t use here or you’ll end up with compiler errors.

{
  "aps" : {
    "alert" : {
        "title" : "New Calendar Invitation"
    },
    "badge" : 1,
    "mutable-content" : 1,
    "category" : "CalendarInvite"
  },
  "title" : "Family Reunion",
  "start" : "2018-04-10T08:00:00-08:00",
  "end" : "2018-04-10T12:00:00-08:00",
  "id" : 12
}
let content = UNMutableNotificationContent()
content.title = "New Calendar Invitation"
content.badge = 1
content.categoryIdentifier = "CalendarInvite"
content.userInfo = [
  "title": "Family Reunion",
  "start": "2018-04-10T08:00:00-08:00",
  "end": "2018-04-10T12:00:00-08:00",
  "id": 12
]

Sounds

If you’d like your notification to play a sound when it’s delivered, you must either store the file in your app’s main bundle, or you must download it and store it in the Library/Sounds subdirectory of your app’s container directory. Generally, you’ll just want to use the default sound:

content.sound = UNNotificationSound.default()

Localization

There’s one small “gotcha” when working with localization and local notifications. Consider the case wherein the user’s device is set to English, and you set the content to a localized value. Then, you create a trigger to fire in three hours. An hour from then, the user switches their device back to Arabic. Suddenly, you’re showing the wrong language!

Grouping

If you’d like your local notification to support grouping, simply set the threadIdentifier property with a proper identifier to group them by.

content.threadIdentifier = "My group identifier here"

Scheduling

Now that you’ve defined when the notification should occur and what to display, you simply need to ask iOS to take care of it for you:

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

UNUserNotificationCenter.current().add(request) { error in 
  if let error = error {
    // Handle unfortunate error if one occurs.
  }
}

Foreground notifications

Just like with remote notifications, you’ll need to take an extra step to allow local notifications to be displayed when the app is running in the foreground. It’s the exact same code that remote notifications use.

func userNotificationCenter(
  _ center: UNUserNotificationCenter, 
  willPresent notification: UNNotification, 
  withCompletionHandler completionHandler: 
  @escaping (UNNotificationPresentationOptions) -> Void) {
  
  completionHandler([.badge, .sound, .alert])
}

The sample platter

That seems like quite enough reference material. Time to write some code! Please open up the starter project and enable push notifications as described in Chapter 4, “Xcode Project Set Up,” and set your team ID as discussed in Chapter 7, “Expanding the Application.”

Configuring the main UITableView

Just like with remote notifications, the first task you’ll need to take care of is getting your user’s permission to send them notifications.

center.requestAuthorization(
  options: [.alert,.sound,.badge],
  completionHandler: { [weak self] (granted, error) in
    guard let self = self else { return }

    if granted {
      self.refreshNotificationList()
      self.center.delegate = self
    }

    self.addButton.isEnabled = granted
    self.refreshButton.isEnabled = granted
})  
center.getPendingNotificationRequests { [weak self] requests in
  guard let self = self else { return }

  self.pending = requests
  DispatchQueue.main.async {
    self.tableView.reloadData()
  }
}
center.getDeliveredNotifications { [weak self] requests in
  guard let self = self else { return }

  self.delivered = requests
  DispatchQueue.main.async {
    self.tableView.reloadData()
  }
}
let identifiers = [request.identifier]
center.removePendingNotificationRequests(
  withIdentifiers: identifiers)
let identifiers = [request.identifier]
center.removeDeliveredNotifications(
  withIdentifiers: identifiers)

Scheduling

While there are more options available on the content of a notification, for the sample app, you’ll only be using the title, sound and badge properties of the UNMutableNotificationContent. As a good programmer, you always follow the KISS principle, right?

Creating content

You’ll create the content in scheduleNotification. You’ve already been passed the appropriate UNNotificationTrigger, so now you’ll need to generate the content that goes with it.

let content = UNMutableNotificationContent()
content.title = title

if sound {
  content.sound = UNNotificationSound.default
}

if let badge = badge, let number = Int(badge) {
  content.badge = NSNumber(value: number)
}

Adding the request

Now that the content and trigger are in place, all that’s left to do is create the request and hand it off to UNUserNotificationCenter. You’ve already seen the code for this, so it shouldn’t be anything too shocking. Add the following to the end of the method:

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

UNUserNotificationCenter.current().add(request) { 
  [weak self] error in
  
  guard let self = self else { return }

  if let error = error {
    DispatchQueue.main.async {
      let message = """
        Failed to schedule notification.
        \(error.localizedDescription)
      """
      UIAlertController.okWithMessage(message, 
        presentingViewController: self)
    }
  } else {
    DispatchQueue.main.async {
      self.navigationController?.popToRootViewController(
        animated: true)
    }
  }
}

Time interval notifications

You’re almost ready to run the app and see something! The first location notification trigger to implement is the UNTimeIntervalNotificationTrigger. With the extension you just created, you’ll only need two lines of code now to set up a time-interval trigger. Open TimeIntervalViewController.swift and take a look at the doneButtonTouched method. Once the number of seconds to wait is known, you need to create the trigger, just like you learned about a few pages back. Add this code to the end of the method:

let trigger = UNTimeIntervalNotificationTrigger(
  timeInterval: interval,
  repeats: repeats.isOn)

scheduleNotification(
  trigger: trigger, 
  titleTextField: notificationTitle,
  sound: sound.isOn, 
  badge: badge.text)

Location notifications

Handling locations takes just a bit more work.

Location permissions

In order to get the location to trigger, you need to know the user’s location. This means you first need to ask the user’s permission to access their location.

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)

  // 1
  address.isEnabled = false
  doneButton.isEnabled = false

  switch CLLocationManager.authorizationStatus() {
  
  // 2
  case .notDetermined:
    locationManager.requestWhenInUseAuthorization()
    
  // 3
  case .restricted:
    let message = 
      "This device is not allowed to use location services."
    UIAlertController.okWithMessage(message,
      presentingViewController: self)
  case .denied:
    let message = "Location services must be enabled."
    UIAlertController.okWithMessage(
      message,
      presentingViewController: self)
  
  // 4
  case .authorizedWhenInUse:
    address.becomeFirstResponder()
    address.isEnabled = true
    doneButton.isEnabled = true

  default:
    break
  }

  locationManager.startUpdatingLocation()
}
let region = CLCircularRegion(
  center: coordinate, 
  radius: distance,
  identifier: UUID().uuidString)
region.notifyOnExit = notifyOnExit.isOn
region.notifyOnEntry = notifyOnEntry.isOn

let trigger = UNLocationNotificationTrigger(
  region: region, 
  repeats: repeats.isOn)

scheduleNotification(
  trigger: trigger, 
  titleTextField: notificationTitle,
  sound: sound.isOn, 
  badge: badge.text)

Calendar notifications

Just one notification to go! Calendar-based local notifications, as discussed earlier, use a DateComponents struct to specify exactly when the notification will trigger. If you’ve worked with DateComponents before, you know how many different properties are available to you. For the sample app, to keep things simple, you’re just using hours, minutes and seconds. In CalendarViewController.swift, you’ll see that the doneButtonTouched method has pulled out the details of the time for you already. All you’ve got to do is create the trigger to fire at the right time. Add the following code at the end of the method:

let trigger = UNCalendarNotificationTrigger(
  dateMatching: components,
  repeats: repeats.isOn)

scheduleNotification(
  trigger: trigger, 
  titleTextField: notificationTitle,
  sound: sound.isOn, 
  badge: badge.text)

Key points

  • While most push notifications displayed on your device are remote notifications, it’s also possible to display notifications originating from the user’s device, locally.
  • Local notifications are less frequently used but they still merit your understanding. There may be times when a user needs a notification (like a reminder) free of any action being taken.
  • Calendar notifications occur on a specific date.
  • Interval notifications occur after a specific amount of time.
  • Location notifications occur when entering a specific area.
  • Even though the notification is created and delivered locally, on the user’s device, you must still obtain permission to display notifications.

Where to go from here?

In your own apps, you’ll likely want to explore other concepts such as custom sounds, more options around calendar selection, and even custom actions and user interfaces. Refer back to each of the following chapters for information on how to add each feature to your local notifications.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.