Chapters

Hide chapters

Push Notifications by Tutorials

Fourth Edition · iOS 16 · Swift 5 · Xcode 14

Section I: Push Notifications by Tutorials

Section 1: 15 chapters
Show chapters Hide chapters

13. Local Notifications
Written by Scott Grosch

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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]) { granted, _  in
    guard granted 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. If you’re using SwiftUI, check for permissions inside the onAppear(perform:) method on your root view.

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 Swift 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. If you’re using a calendar trigger, it’s more likely that you only have parts of a date.

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
]

Playing 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

Adding 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 Notifications

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 {
    // 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)
  async -> UNNotificationPresentationOptions
{
  return [.badge, .sound, .banner]
}

The Sample Platter

That seems like quite enough reference material. Time to write some code! Please open up the starter project and set your team ID as discussed in Chapter 7, “Expanding the Application.”

Requesting Permission

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

func requestAuthorization() async throws {
  authorized = try await center.requestAuthorization(options: [.badge, .sound, .alert])
}

Determine Pending and Delivered Notifications

The stated goal of the app was to display both delivered and pending notifications. To identify all notifications which are pending, you’ll use the pendingNotificationRequests method.

@Published var pending: [UNNotificationRequest] = []
@MainActor
func refreshNotifications() async {
  pending = await center.pendingNotificationRequests()
}
@Published var delivered: [UNNotification] = []
delivered = await center.deliveredNotifications()

Removing Notifications

Most well written apps, which display a list of items, will also provide a way to delete items. The user might have made a mistake in scheduling the notification, for example. Add the following methods to LocalNotifications.swift:

func removePendingNotifications(identifiers: [String]) async {
  center.removePendingNotificationRequests(withIdentifiers: identifiers)
  await refreshNotifications()
}

func removeDeliveredNotifications(identifiers: [String]) async {
  center.removeDeliveredNotifications(withIdentifiers: identifiers)
  await refreshNotifications()
}

Configuring the Main View

It’s time to make use of the class you just created. Open up ContentView.swift and add the following line to the top of the class:

@StateObject private var localNotifications = LocalNotifications()
.sheet(isPresented: $showSheet) {
  Task { await localNotifications.refreshNotifications() }
.task { try? await localNotifications.requestAuthorization() }
if !localNotifications.authorized {
  Text("This app only works when notifications are enabled.")
} else {
}
List {
  // 1
  Section(header: Text("Pending")) {
    // 2
    ForEach(localNotifications.pending, id: \.identifier) {
      // 3
      HistoryCell(for: $0)
    }
  }
}
// 4
.listStyle(GroupedListStyle())
Section(header: Text("Delivered")) {
  ForEach(localNotifications.delivered, id: \.request.identifier) {
    HistoryCell(for: $0.request)
  }
}
.onDelete(perform: deletePendingNotification)
private func deletePendingNotification(at offsets: IndexSet) {
  let identifiers = offsets.map {
    localNotifications.pending[$0].identifier
  }

  Task {
    await localNotifications.removePendingNotifications(identifiers: identifiers)
  }
}
.navigationBarItems(trailing: Button {
.navigationBarItems(leading: EditButton(), trailing: Button {

Scheduling Notifications

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.

Creating Content

Edit the LocalNotifications.swift file to add the following code to the bottom of the class:

func scheduleNotification(trigger: UNNotificationTrigger, model: CommonFieldsModel) async throws {
  let title = model.title.trimmingCharacters(in: .whitespacesAndNewlines)

  let content = UNMutableNotificationContent()
  content.title = title.isEmpty ? "No Title Provided" : title

  if model.hasSound {
    content.sound = UNNotificationSound.default
  }

  if let number = Int(model.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)

try await center.add(request)
Task {
  do {
    try await localNotifications.scheduleNotification(trigger: trigger, model: commonFields)
  } catch {
    alertText = AlertText(text: error.localizedDescription)
  }
}

Time Interval Notifications

You’re almost ready to run the app and see something! The first notification trigger to implement is the UNTimeIntervalNotificationTrigger. With the methods you just created, you’ll only need two lines of code now to set up a time-interval trigger. Open TimeIntervalView.swift and take a look at doneButtonTapped . Once the number of seconds to wait is known, you need to create the trigger just like you learned about earlier in the chapter.

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

try await onComplete(trigger, model)

extension LocalNotifications: UNUserNotificationCenterDelegate {
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification
  ) async -> UNNotificationPresentationOptions {
    return [.banner, .badge, .sound]
  }
}
override init() {
  super.init()
  center.delegate = self
}

Location Notifications

Handling locations takes just a bit more work.

Requesting Location Permissions

To enable the location to trigger a notification, you need to know the user’s location. This means you first need to ask the user’s permission to access their location.

@EnvironmentObject private var locationManager: LocationManager
if locationManager.authorized {
  LocationForm(onComplete: onComplete)
} else {
  Text(locationManager.authorizationErrorMessage)
}
Group {
  if locationManager.authorized {
    LocationForm(onComplete: onComplete)
  } else {
    Text(locationManager.authorizationErrorMessage)
  }
}
.onAppear(perform: locationManager.requestAuthorization)

guard let coordinates = model.coordinate else {
  return
}

let region = CLCircularRegion(
  center: coordinates,
  radius: distance,
  identifier: UUID().uuidString)

region.notifyOnExit = model.notifyOnExit
region.notifyOnEntry = model.notifyOnEntry

let trigger = UNLocationNotificationTrigger(region: region, repeats: commonFields.isRepeating)
try await onComplete(trigger, commonFields)

Calendar Notifications

Just one notification to go! Calendar-based local notifications, as discussed earlier, use the 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.

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

try await onComplete(trigger, commonFields)

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 or time.
  • 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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now