Chapters

Hide chapters

Push Notifications by Tutorials

Second Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Push Notifications by Tutorials

Section 1: 14 chapters
Show chapters Hide chapters

8. Handling Common Scenarios
Written by Scott Grosch

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

Displaying foreground notifications

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.

Note: After opening up the starter project for this chapter, remember to turn on the Push Notifications capability as discussed in Chapter 4, “Xcode Project Setup,” and set the team signing 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([.alert, .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.

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.

Go back into your AppDelegate.swift file and add the following UNUserNotificationCenterDelegate method at the bottom of your extension:

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

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

Notice again that there is a completion handler that must be called before exiting the method. This is a great use case for Swift’s defer keyword as you’re ensuring the block of code will be run no matter how you leave the method.

Right now, this method doesn’t make much sense as-is. In the next chapter, when you read about custom actions, you’ll come back to expand on this. If you don’t need to take any custom actions when the user dismisses or taps on your notifications, you can simply omit this method as it’s optional in the delegate definition.

Note: There is an actionIdentifier called UNNotificationDismissActionIdentifier. Don’t be fooled into thinking this method will be called if the user simply dismisses the notification!

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.

Sometimes, that’s not what you want though, as the notification should take you to a specific view controller within your app. This delegate method is exactly where you’ll handle that routing.

Since your delegate is growing at this point, you should get it out of the AppDelegate.swift file. Obviously, this is a matter of personal style and preference, but keeping a clear separation of duties is always a good idea.

Create a new Swift file called NotificationDelegate.swift and then move your delegate methods to that new file. Since UNUserNotificationCenterDelegate depends on NSObjectProtocol, you’ll have to define your class as inheriting from NSObject.

import UIKit
import UserNotifications

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

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

    guard 
      response.actionIdentifier == UNNotificationDefaultActionIdentifier 
    else {
      return
    }

    // Perform actions here
  }
}

Back in AppDelegate.swift, there are just three quick steps to take:

  1. Remove the entire UNUserNotificationCenterDelegate extension.

  2. Remove the import UserNotifications line.

  3. Add a delegate property to the AppDelegate class: let notificationDelegate = NotificationDelegate().

Then hop over to ApnsUploads.swift and change the assignment of the delegate in registerForPushNotifications to use your new object:

center.delegate = self?.notificationDelegate

Do a quick build of your project just to make sure you didn’t miss any steps. There should be no warnings or errors at this point.

For the example, if your payload contains a beach key, then you want to land directly on the BeachViewController location of your app. To keep everything simple, the starter project already includes the view controller and a pretty beach image. Normally, your payload would specify the image URL to download and display in the view controller itself.

In NotificationDelegate.swift’s userNotificationCenter(_:didReceive:withCompletionHandler:), examine the payload and take them to the right spot if the key exists.

Below the // Perform actions here comment, add:

let payload = response.notification.request.content
guard payload.userInfo["beach"] != nil else { return }

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "beach")

let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window!.rootViewController!.present(vc, animated: false)

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", instantiate the BeachViewController from a storyboard and present it on top of the root view controller of the window.

You can see how, in a more dynamic setup, the userInfo might contain a URL to an image that you may configure on the view controller itself.

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 BeachViewController instantiated above:

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:

  1. Update the payload.
  2. Add the Background Modes capability.
  3. Implement a new UIApplicationDelegate method.

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/2Iodl06",
  "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 0 thinking you’ve disabled this. If you don’t want a silent notification — do not include the content-available key!

Note: Remember to set the apns-priority 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 Capabilities tab, toggle on the Background Modes option and then choose the Remote notifications checkbox from the bottom of the list.

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 your UIApplicationDelegate extension in AppDelegate.swift:

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
  }
}

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 issue, you can just tell iOS that you don’t have the needed data by passing .noData to your completionHandler.

You probably don’t want to specify .failed since technically this just wasn’t a payload for an image.

Next, add the following code below the guard statement in the method:

// 1
let context = persistentContainer.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)
  }
}

Here’s what’s going on in the code:

  1. 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.
  2. You create a new Message object and download the image.
  3. Since you did, in fact, receive data, you’ll want to tell iOS that you got new data from this notification, and that you were able to successfully process the notification.
  4. If anything went wrong, you’ll 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 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.
© 2026 Kodeco Inc.