8.
Handling Common Scenarios
Written by Scott Grosch
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.
You are able to configure parts of the notification via a UNUserNotificationCenterDelegate. To support a delegate, replace the register(in:) method back in PushNotifications.swift with this method:
static func register(
in application: UIApplication,
// 1
using notificationDelegate: UNUserNotificationCenterDelegate? = nil
) {
Task {
let center = UNUserNotificationCenter.current()
try await center.requestAuthorization(options: [.badge, .sound, .alert])
// 2
center.delegate = notificationDelegate
await MainActor.run {
application.registerForRemoteNotifications()
}
}
}
You made two simple changes:
- The method now accepts an optional delegate.
- You assigned the
UNUserNotificationCenter’s delegate to be the supplied delegate.
The delegate you will implement is pretty simple. Create a new file, NotificationCenter.swift and replace the file’s contents with the following code:
import UserNotifications
final class NotificationCenter: NSObject {
}
extension NotificationCenter: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
}
Probably one of the most complex methods you’ve ever written, right? In just a bit you’ll need this class to conform to NSObject so I’m simply having you define it that way now.
The method you implemented gets called by iOS when it is about to show a notification. In the method, you’re 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 if you wanted the notification to display to the end user. As of iOS 14, Apple 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 .banner enum value. If you only wish alerts to appear when the app is running in the background, use .list.
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.
Finally, back in AppDelegate.swift, add a new property to the class:
let notificationCenter = NotificationCenter()
And then update the register(in:) call to utilize the delegate:
PushNotifications.register(in: application, using: notificationCenter)
Build and run your app. Now, use the tester app (as described in Chapter 5, “Sending Your First Push Notification”) 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.
Handling 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.
Sometimes, that’s not what you want though, as the notification should take you to a specific view within your app. Add the following property to the NotificationCenter class:
@Published var isBeachViewActive = false
Then, tell the class to conform to the ObservableObject protocol:
extension NotificationCenter: ObservableObject {}
Now, inside the extension, add the delegate method to handle taps:
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
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.
await MainActor.run {
isBeachViewActive = true
}
}
}
The keys sent with the push notification are inside the userInfo property, a simple Swift dictionary. If you find a value with the key "beach", set the published property to true. You can see how, in a more dynamic setup, the userInfo might contain a URL to an image that you may then need to download. You’ll handle a case similar to this later in this book.
Remember that userNotificationCenter(_:didReceive:) is not called on the main thread, and so you must delegate back to the main thread via the await MainActor.run call.
Hop over to PushNotificationsApp.swift. Add the following line at the end of body, right under the environment call:
.environmentObject(appDelegate.notificationCenter)
This will give all child views access to the notification delegate, including ContentView, which you’ll update next.
Jump over to ContentView.swift and add a new property to the struct:
@EnvironmentObject var notificationCenter: NotificationCenter
This will grab the notification delegate you just added to the app struct.
Finally, add the following code to body, at the bottom of NavigationStack:
if notificationCenter.isBeachViewActive {
BeachView()
}
When the newly created property in NotificationCenter gets set to true, you will show the beach view.
Build and run your app, then send yourself a test push with the following payload:
{
"beach": true,
"aps": {
"alert": {
"body": "Tap me!"
}
}
}
Once the notification is presented, tap it. If all goes well, you should be presented with the BeachView instantiated above:
Sending 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.
These are generally referred to as silent notifications, but what they really mean is, “Hey app, there’s new content available on the server you might need to do something with.”
If you’ve written an RSS reader app, for example, you might send a silent notification when a new post is submitted so that the app can prefetch the data.
This makes the user’s app experience much quicker as the data is there as soon as the app is opened, versus the end user watching an activity indicator while the article is being downloaded.
There are three distinct steps you have to take in order to enable silent notifications:
- Update the payload.
- Add the Background Modes capability.
- Implement a new
UIApplicationDelegatemethod.
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.
In this case, you’re going to have your app prefetch an image. To start, create a payload like so:
{
"aps": {
"content-available": 1
},
"image": "https://bit.ly/3dfsW2n",
"text": "A nice picture of the Earth"
}
You can use any image URL you’d like. The above is just a known image that should always resolve.
Note: Don’t set the value to
0thinking you’ve disabled this. If you don’t want a silent notification — do not include thecontent-availablekey!
Note: Remember to set the
apns-priorityHTTP header to 5, as explained in Chapter 3.
Adding Background Modes Capability
Next, back in Xcode, you’ll need to add a new capability just as you did at project creation.
Open the project navigator (⌘ + 1), select your project and then select your app target.
Now, on the Signing & Capabilities tab, press the + Capability button and add the Background Modes capability. From the Background Modes options check the Remote notifications checkbox.
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.
You’ll need to implement a new AppDelegate method by adding following code in AppDelegate.swift:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any])
async -> UIBackgroundFetchResult {
guard
let text = userInfo["text"] as? String,
let image = userInfo["image"] as? String,
let url = URL(string: image)
else {
completionHandler(.noData)
return
}
}
This method gets called whenever a push notification comes in, including when your app is in the background. You are expecting both text and an image as part of the payload, and you need to ensure that the image specified is actually something you can turn into a URL.
If there is any issues, you can tell iOS that you don’t have the needed data by returning .noData. You probably don’t want to specify .failed since technically this just wasn’t a payload for an image.
Since you’re about to update Core Data objects you’ll need to import the appropriate module at the top of the file:
import CoreData
Next, add the following code below the guard statement in the method:
let context = PersistenceController.shared.container.viewContext
do {
// 1
let (imageData, _) = try await URLSession.shared.data(from: url)
// 2
return try await context.perform(schedule: .immediate) { () throws -> UIBackgroundFetchResult in
// 3
let message = Message(context: context)
message.image = imageData
message.received = Date()
message.text = text
try context.save()
// 4
return .newData
}
} catch {
// 5
return .failed
}
Here’s what’s going on in the code:
- First, fetch the image data from the URL.
- Which thread is your notification running on? Not sure? Play it safe and make sure the Core Data operations run on the proper thread of your Core Data persistent container.
- Create a new
Messageobject wit the downloaded image. - Since you did, in fact, receive data, tell iOS that you got new data from this notification, and that you were able to successfully process the notification.
- If anything went wrong, tell iOS that processing the notification failed.
Note: iOS will wake up your app in the background and give it up to 30 seconds to complete whatever actions you need to take. Make sure you perform the minimal amount of work necessary so that your action can complete in time.
Build and run the project.
Send yourself a few more silent push notifications using different images and text, and you should see your table updating appropriately.
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
UNUserNotificationCenterDelegatemethod, 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.