Home iOS & Swift Books Push Notifications by Tutorials

8
Handling Common Scenarios Written by Scott Grosch

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

So far you learned how to receive remote push notifications from APNs. iOS then takes over and shows the notification to the user. However, that’s not the full story. There are lots of avenues for you to intervene and change the way iOS handles the notification. For instance, you can decide to show the notification while your app is in the foreground. You can also decide what happens when your user taps the notification. Or, you can hide the notification from your user entirely. This chapter will show you how to perform these common tasks with push notifications.

Displaying foreground notifications

As you noticed in previous projects in this book, iOS will automatically handle presenting your notifications as long as your app is in the background or terminated. But what happens when it is actively running? In that case, you need to decide what it is that you want to happen. By default, iOS simply eats the notification and never displays it. That’s pretty much always what you want to happen, right? No? Didn’t think so!

In the download materials for this chapter, you’ll find possibly the coolest starter project that’s ever been created.

sarcasm
ˈsär-ˌka-zəm
noun
the use of irony to mock or convey contempt

If you’d like to have iOS display your notification while your app is running in the foreground, you’ll need to implement the UNUserNotificationCenterDelegate method userNotificationCenter(_:willPresent:withCompletionHandler:), which is called when a notification is delivered to your app while it’s in the foreground. The only requirement of this method is calling the completion handler before it returns. Here, you can identify what you want to happen when the notification comes in.

Open the starter project from this chapter’s download materials. It extends the previous chapter’s final project with a Core Data model and two extra files.

Note: After opening up the starter project for this chapter, remember to set the development team as discussed in Chapter 7, “Expanding the Application.”

Conform to the aforementioned protocol in your AppDelegate. At the bottom of AppDelegate.swift, write the following:

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

Probably one of the most complex methods you’ve ever written, right?

You’re simply telling the app that you want the normal alert to be displayed, the sound played and the badge updated. If the notification doesn’t have one of these components, or the user has disabled any of them, that part is simply ignored.

It used to be that you’d specify .alert in your completion handler if you wanted the notification to display to the end user. As of iOS 14, Apple now provides you the ability to decide whether or not you’d like the alert to display when the app is in the foreground. If you do want foreground notifications, choose the new .banner enum value. If you only wish alerts to appear when the app is running in the background, use the new .list enum.

If you want no action to happen, you can simply pass an empty array to the completion closure. Depending on the logic that pertains to your app, you may want to investigate the notification.request property of type UNNotificationRequest and make the decision about which components to show based on the notification that was sent to you.

In order for the delegate to be called, you have to tell the notification center that the AppDelegate is the actual delegate to use.

Make a couple changes to your registerForPushNotifications(application:) back in ApnsUploads.swift:

func registerForPushNotifications(application: UIApplication) {
  let center = UNUserNotificationCenter.current()
  center.requestAuthorization(options: [.badge, .sound, .alert]) {
    // 1
    [weak self] granted, _ in

    // 2
    guard granted else {
      return
    }

    // 3
    center.delegate = self

    DispatchQueue.main.async {
      application.registerForRemoteNotifications()
    }
  }
}

There are three simple changes made:

  1. Capture a weak reference to self in the completion handler.
  2. Then, make sure you have been granted the proper authorization to register for notifications.
  3. Finally, you just need to set the UNUserNotificationCenter’s delegate to be the AppDelegate object.

Build and run your app. Now, use the tester app (as described in Chapter 5, “Apple Push Notifications Servers”) to send a push notification while you’re in the foreground. You should see it displayed this time! You can use the following simple payload for testing purposes:

{
  "aps": {
    "alert": {
      "title": "Hello Foreground!",
      "body": "This notification appeared in the foreground."
    }
  }
}

You should get a notification on your device with your app still in the foreground!

Tapping the notification

The vast majority of the time when a push notification arrives, your end users won’t do anything except glance at it. Good notifications don’t require interaction, and your user gets what they need at a glance. However, that’s not always the case. Sometimes your users actually tap on the notification, which will trigger your app to be launched.

func userNotificationCenter(
  _ center: UNUserNotificationCenter, 
  didReceive response: UNNotificationResponse, 
  withCompletionHandler completionHandler: @escaping () -> Void
) {
  defer { completionHandler() }

  guard response.actionIdentifier 
    == UNNotificationDefaultActionIdentifier else {
    return
  }
  
  // Perform actions here
}

Handle user interaction

By default, tapping on the notification simply opens up your app to whatever the “current” screen was — or the default startup screen, if the app was launched from a terminated state.

import UserNotifications

final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
  func userNotificationCenter(
    _ center: UNUserNotificationCenter, 
    willPresent notification: UNNotification, 
    withCompletionHandler completionHandler: 
    @escaping (UNNotificationPresentationOptions) -> Void
  ) {
    completionHandler([.banner, .sound, .badge])
  }

  func userNotificationCenter(
    _ center: UNUserNotificationCenter, 
    didReceive response: UNNotificationResponse, 
    withCompletionHandler completionHandler: @escaping () -> Void
  ) {
    defer { completionHandler() }

    guard 
      response.actionIdentifier == UNNotificationDefaultActionIdentifier 
    else {
      return
    }

    // Perform actions here
  }
}
let notificationDelegate = NotificationDelegate()
center.delegate = self?.notificationDelegate
final class NotificationDelegate: NSObject, 
  UNUserNotificationCenterDelegate, ObservableObject {
@Published var isBeachViewActive = false
if response.notification.request.content.userInfo["beach"] != nil {
  // In a real app you'd likely pull a URL from the beach data
  // and use that image.
  isBeachViewActive = true
}
.environmentObject(appDelegate.notificationDelegate)
@EnvironmentObject var notificationDelegate: NotificationDelegate
NavigationView {
  // your currrent body implementation
}
NavigationLink(
  destination: BeachView(),
  isActive: $notificationDelegate.isBeachViewActive) {
  EmptyView()
}
{
  "beach": true,
  "aps": {
    "alert": {
      "body": "Tap me!"
    }
  }
}

Silent notifications

Sometimes, when you send a notification, you don’t want the user to actually get a visual cue when it comes in. No alert or sound, for example.

Updating the payload

The first step to take is simply adding a new key-value pair to your payload. Inside of the aps dictionary, add a new key of content-available with a value of 1. This will tell iOS to wake your app when it receives a push notification, so it can prefetch any content related to the notification.

{
  "aps": {
    "content-available": 1
  },
  "image": "https://bit.ly/3dfsW2n",
  "text": "A nice picture of the Earth"
}

Adding background modes capability

Next, back in Xcode, you’ll need to add a new capability just as you did at project creation.

App delegate updates

When a silent notification comes in, you’ll want to make sure that it contains the data you’re expecting, updates your Core Data model, and then tells iOS you’re done processing.

func application(
  _ application: UIApplication, 
  didReceiveRemoteNotification userInfo: [AnyHashable: Any], 
  fetchCompletionHandler completionHandler: 
  @escaping (UIBackgroundFetchResult) -> Void
) {
  guard 
    let text = userInfo["text"] as? String,
    let image = userInfo["image"] as? String,
    let url = URL(string: image) else {
    completionHandler(.noData)
    return
  }
}
import CoreData
// 1
let context = PersistenceController.shared.container.viewContext
context.perform {
  do {
    // 2
    let message = Message(context: context)
    message.image = try Data(contentsOf: url)
    message.received = Date()
    message.text = text

    try context.save()
    // 3
    completionHandler(.newData)
  } catch {
    // 4
    completionHandler(.failed)
  }
}
@Environment(\.managedObjectContext) private var managedObjectContext

Method routing

The following table shows you which methods are called, and in what order, depending on whether your app is in the foreground or background, and whether or not the content-available flag (i.e., silent notification) is present with a value of 1.

Key points

  • For iOS to display your notification while your app is running in the foreground, you’ll need to implement a UNUserNotificationCenterDelegate method, which is called when a notification is delivered to your app while it’s in the foreground.
  • Good notifications don’t require interaction, and your user gets what they need at a glance. Some notifications are tapped, however, which triggers an app launch. You will need to add an additional method in your AppDelegate.swift file.
  • Sometimes, you want a tapped notification to open a specific view controller within your app. You will need to add an additional method to handle this routing.
  • Silent notifications give no visual or audible cue. To enable silent notifications, you’ll need to update the payload, add the Background Modes capability, and implement a new UIApplicationDelegate method.

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 scrambled 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.