Handling iOS Push Notifications: The Not So Apparent Side

Oscar
Stories by Fenrir Inc.
8 min readSep 7, 2018

At Fenrir, many of our developers have had the experience of maintaining applications that have been around for a while. Because of the scale of some of these apps, supporting older iOS versions is often a requirement. When you also consider the fact that a small mistake in the migration could possibly impact thousands of people, understanding what’s going on with the code becomes a necessity. Push notifications is one of those technologies that can be hard to test because of the setup required.

Since push notifications have been around for quite a while, this article will not talk about how to implement them. Instead, it will try to clarify various aspects about push notifications that might not be so apparent to newcomers or might have even been overlooked by some veterans. One of them being the timing in where each of the handler methods is actually called.

Configuration

Before push notifications can be received there are 2 things that must be properly configured.

The Device Capabilities

The app must specify what capabilities it supports. To receive push notifications, the “Push Notifications” capability must be turned on.

Another capability that can also be turned on is “Remote notifications”. This one is found under the “Background Modes” group and will allow your app to be launched in the background in response to a notification whose payload includes the “content-available” flag (aka Silent Notifications).

The Device Token

Getting a device token is necessary so that your server can use it to send notifications through apple’s distribution system.

The device token can be retrieved by calling this method:

Which will return the appropriate response in your AppDelegate methods.

However, the timing in where this method should be called depends on the device capabilities currently set.

That is, if you don’t have the “Remote notifications” capability turned on, you will have to ask for the user permission to display alerts before being able to retrieve the device token. But if it is on, you can retrieve the device token at any time.

Asking the user for permission is done with the following methods.

Before iOS 10, using the UIApplication method:

  • registerUserNotificationSettings(_:)

iOS 10 onwards, using the UserNotifications framework methods:

  • requestAuthorization(options:completionHandler:)
  • setNotificationCategories(_:)

Notification Types

Something that is often overlooked is that there are actually 2 types of push notifications.

“Alert Notifications”

These notifications are used to alert or inform the user about something. They can include an alert, set a badge and even play a specific sound.

  • Only the “Push Notifications” capability is required.
  • The user’s permission is required before retrieving the device token.

Their payload is similar to this (example taken from the apple documentation):

{
"aps" : {
"alert" : "You got your emails.",
"badge" : 9,
"sound" : "bingbong.aiff"
},
"acme1" : "bar",
"acme2" : 42
}

Important:
If the payload doesn’t include at least one of the following 3 keys, “alert”, “badge”, “sound” then the push notification will be ignored. (No methods will be called inside your app)

“Silent Notifications”

These notifications are used to execute code in response to a push notification event. They do not show an alert. They also work when your app is on the background sleeping, or when it isn’t running (was killed by the system). In these cases it will be awaken/relaunched and given up to 30 seconds of execution time.

  • The “Remote notifications” capability must be set in addition to the “Push Notifications” one.
  • The device token can be retrieved as soon as your app is launched.

The payload for silent notifications must include the “content-available” flag set to “1”.

{
"aps" : {"content-available" : 1},
"acme2" : [ "bang", "whiz" ]
}

Important:
Your app won’t be launched if it has been killed by the user.

Mixed Push Notifications

This is not a notification type per-se but it’s important to mention that the payload of both notification types can be mixed.

{
"aps" : {
"alert" : "You got your emails.",
"badge" : 9,
"sound" : "bingbong.aiff",
"content-available" : 1
},
"acme1" : "bar",
"acme2" : 42
}

Important:
Just mixing the payload is not enough. You still have to configure and implement the required methods for each of the types independently.

The Methods

When a push notification arrives there are 4 methods that can be used to handle the notification. Which method gets called is defined by the following factors:

  1. The currently implemented methods.
  2. The current application state.
  3. The notification payload.

The next section will try to explain this behavior. For simplicity, the different application states will be summarized like this:

  • Foreground: The app is currently shown on the user’s screen.
  • Background: The app is not shown on the screen but it hasn’t been terminated by the user or the system.
  • Not Running: The app has been terminated by the system.
  • Not Running (killed by the user): The app has been terminated by the user.

And the notification payload type will be summarized like this:

  • Alert Payload: Contains a least one of the following values “alert”, “badge”, “sound”.
  • Silent Payload: Has the “content-available” key set to “1”. (It is assumed that the remote notifications background capability is enabled)
  • Mixed Payload: Alert Payload + Silent Payload

Finally, the methods can be categorized in 2, based on where they should be implemented.

The AppDelegate Methods

These methods can be used pre iOS 10. They are implemented inside the application’s AppDelegate class since they are part of the UIApplicationDelegate protocol.

application(_:didReceiveRemoteNotification:)

The oldest among the methods when handling push notifications.

Things to consider:

  • It was deprecated on iOS 10.
  • It’s use is limited so the UserNotifications framework should be used instead.

With an Alert Payload it is called in the following situations:

  • Foreground: When the notification arrives. (Doesn’t show a system notification)
  • Background: If the user taps the notification, before the app enters foreground.
  • Not Running: Not called.
  • Not Running (killed by user): Not called.

With a Silent Payload it is called in the following situations:

  • Foreground: When the notification arrives.
  • Background: When the notification arrives.
  • Not Running: After the app enters foreground.
  • Not Running (killed by user): Not called.

With a Mixed Payload it is called in the following situations:

  • Foreground: When the notification arrives. (Doesn’t show a system notification)
  • Background: When the notification arrives. + If the user taps the notification, before the app enters foreground.
  • Not Running: After the app enters foreground.
  • Not Running (killed by user): Not called.

application(_:didReceiveRemoteNotification:fetchCompletionHandler:)

The purpose of this method is to handle silent notifications, but it can also be used if the UserNotifications framework is not available (pre iOS 10).

Things to consider:

  • application(_:didReceiveRemoteNotification:) won’t be called if this method is implemented.
  • Gives a warning if the “Remote notifications” capability is not enabled.
  • You must call the block handler once the background operation has been completed.

With an Alert Payload it is called in the following situations:

  • Foreground: When the notification arrives. (Doesn’t show a system notification)
  • Background: If the user taps the notification, before the app enters foreground.
  • Not Running: If the user taps the notification, before the app enters foreground.
  • Not Running (killed by user): If the user taps the notification, before the app enters foreground.

With a Silent Payload it is called in the following situations:

  • Foreground: When the notification arrives.
  • Background: When the notification arrives.
  • Not Running: When the notification arrives.¹
  • Not Running (killed by user): Not called.

With a Mixed Payload it is called in the following situations:

  • Foreground: When the notification arrives. (Doesn’t show a system notification)
  • Background: When the notification arrives. + If the user taps the notification, before the app enters foreground.
  • Not Running: When the notification arrives.¹ + If the user taps the notification, before the app enters foreground.
  • Not Running (killed by user): If the user taps the notification, before the app enters foreground.

Important¹:
Under normal circumstances, the “content-available” flag should launch your app if it isn’t running and wasn’t killed by the user. However, this is ultimately decided by the system so it might not always happen. In that case, this method will be called the next time the user launches your app.

The UserNotifications Framework Methods

This framework is only available when the deployment target is set to iOS 10 or newer. An object conforming to the UNUserNotificationCenterDelegate protocol should be used. This object must be set as the UNUserNotificationCenter’s delegate before the app finishes launching.

You must assign your delegate object to the UNUserNotificationCenter object no later before your app finishes launching. For example, in an iOS app, you must assign it in the application:willFinishLaunchingWithOptions: or application:didFinishLaunchingWithOptions: method.

The protocol contains two methods which are used to handle incoming push notifications:

  • userNotificationCenter(_:willPresent:withCompletionHandler:)
  • userNotificationCenter(_:didReceive:withCompletionHandler:)

Once both methods are implemented, the AppDelegate method application(_:didReceiveRemoteNotification:) won’t be called anymore.

Important:
The UserNotifications framework is only used to handle Alert Payloads and thus, if you want to handle Silent Payloads, you will have to implement the application(_:didReceiveRemoteNotification:fetchCompletionHandler:) method as well.

Understanding when each of these methods gets called can be done by considering two situations.

  1. If the app is currently on the foreground willPresent” will be called as soon as the notification arrives. You can control whether to display an alert or not based what you return on the method’s completion handler.
  2. Whenever the user responds to a notification “didReceive” will be called with the information regarding the action the user took. This method is called regardless of the current app state as long as the user acted on the notification.

The Launch Options Dictionary

Regardless of the whatever methods are currently implemented, if the application was launched as a result of a notification, either by the user tapping on it, or the payload having the “content-available” key, the “launchOptions” dictionary will contain the payload of the notification, which can be accessed through the “.remoteNotification” key.

In other words, you can test if the “launchOptions” dictionary contains a value for the “.remoteNotification” key in case you want to update your interface in response to a notification if your app wasn’t running when it arrived. (Similar to how messaging apps can go straight to a conversation thread when they are launched through a notification)

Conclusion

Even though things have gotten much simpler since the UserNotifications framework was introduced, it can still be useful to know how things used to work before its existence. Especially when having to maintain old projects that still use the old AppDelegate methods or when performing a migration to the UserNotifications framework.

Unfortunately push notifications still remain as one of those technologies that are not quite easy to test and experiment with. We hope this article could offer some clarification about the way they work.

--

--