iOS Push Notification Background Download Demystified

While implementing push notifications, I found a lot of incomplete and inaccurate information out there about how background download works. I had to figure some of this out through experimentation, so in case it can help anyone else, here’s what I’ve learned.

(Note that I’m not going to cover the details of setting up Apple Push Notifications — that’s a whole different topic).

Desired Behavior

The desired behavior, at least for my app, is for the arrival of a push notification to trigger a background download so that when the user next opens the app (either by tapping on the notification, or by opening the app normally), that data is already there.

Additionally, if the user taps on the notification itself, the app should open and display the specific view/data referenced by that notification.

To make this more concrete, in the Emmerge email and task management app, a notification about a new email should trigger the app to download the content of that message from the server, and tapping on the notification should open the app and display that particular thread.

One obvious question is why you can’t just send the desired content directly with the notification. The problem is that Apple restricts the notification payload to 2 kilobytes, which isn’t large enough to include the content and metadata for most emails.

Configuring Your Project for Notification Background Download

To start, you need to set the proper capabilities for your app. I assume you’ve already enabled Push Notifications in the Capabilities section of the target settings. For the background fetch piece, you also need to enable both “Background fetch” and “Remote notifications” under Background Modes.

The All-Important content-available flag

Secondly, don’t forget to set content-available to 1 in the actual notification from your server, to trigger the background download. If you fail to include this flag in the notification, your app will not receive any protocol method calls when the notification arrives.

Understanding the relevant App Delegate protocol methods

At first glance, this seems straightforward. The UIApplicationDelegateProtocol provides the application:didReceiveRemoteNotification:fetchCompletionHandler: method, which is called when the notification arrives on the phone. The complication is two-fold, however:

  1. The exact same didReceiveRemoteNotification method is also called with the exact same parameters when the user taps on the notification to launch the app.
  2. If the app is not already running when the notification arrives (or when the user taps on the notification), the notification information is passed as a launch option to didFinishLaunchingWithOptions, and then didReceiveRemoteNotification is called*. Again without distinguishing notification arrival from user tapping on notification.

Thus, there are four cases you need to handle:

  1. Notification arrives, app running in background.
  2. Notification arrives, app not running in background.
  3. User taps on notification, app running in background.
  4. User taps on notification, app not running in background.

Cases 1 and 3 call didReceiveRemoteNotification only while cases 2 and 4 provide the notification details in the launch options of didFinishLaunchingWithOptions and then call didReceiveRemoteNotification.

As an additional complication, if the user has manually killed your application by swiping it out of memory, case 2 will not occur. That is, your app will never be started in the background to fetch data until after the user chooses to launch it again.

Distinguishing Notification Arrival from User Tapping Notification

With one exception, you can distinguish the cases of notification arrival and user tapping on notification in didReceiveRemoteNotification (or didFinishLaunchingWithOptions) by checking the application state. If the application is running in the background (UIApplicationStateBackground), you should perform a background download. If, instead, the application is “inactive” (UIApplicationStateInactive), it means it’s moving from background to active in response to the user tapping the notification. If the app is already active (UIApplicationStateActive), you generally don’t want to do anything.

However, there is one important edge case. If the app is suspended (in fast app switching mode, after double clicking the home button) and the notification arrives, the state of your app will be UIApplicationStateInactive, not UIApplicationStateBackground. However, this does not mean that the user tapped on the notification. So, you need additional logic to detect this case. You can use applicationWillEnterForeground as an additional piece of information. That method is always called when the app transitions from background to inactive, so you can rely on it being called when user taps on notification and app is actually starting versus just a notification arrival, but no user interaction yet. You can keep a flag to track whether or not the app is actually starting.

Here’s the basic code for this in the AppDelegate:

- (void)applicationWillResignActive:(UIApplication *)application {
self.appIsStarting = NO;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
self.appIsStarting = NO;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
self.appIsStarting = YES;
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
self.appIsStarting = NO;
}
- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    UIApplicationState state = [application applicationState];
    if (state == UIApplicationStateBackground ||
(state == UIApplicationStateInactive &&
!self.appIsStarting)) {
        NSDictionary *aps = userInfo[@”aps”];
if (aps) {
// perform the background fetch and
// call completion handler
}
}
    } else if (state == UIApplicationStateInactive &&
self.appIsStarting) {
        // user tapped notification
completionHandler(UIBackgroundFetchResultNewData);
} else {
        // app is active             
completionHandler(UIBackgroundFetchResultNoData);
}
}

This code handles cases 1 and 3 above.

Handling The Case of App Not Previously Running

As mentioned earlier, if the app is not yet running when the notification arrives, or when the user taps the notification, didFinishLaunchingWithOptions is called with the notification in the launchOptions, before didReceiveRemoteNotification is called. The easiest way to handle this is to ignore the notification options in didFinishLaunchingWithOptions. However, you do need to remember to set the appIsStarting flag in didFinishLaunchingWithOptions so that didReceiveRemoteNotification knows to consider it a notification tap if the application state is UIApplicationStateInactive:

NSDictionary *data = [launchOptions
objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if (data) {
self.appIsStarting = YES;
}

Using the Completion Handler

Don’t forget that you have only 30 seconds to perform the background fetch, and you must call the completionHandler when done.

Testing

Sadly, you can’t test push notifications on the simulator — you have to use a physical device.

In order to test the scenario of the app starting in background mode (rather than already running in the background) when the notification arrives, you can start the app on your phone with Xcode, then kill the app by stopping it in Xcode. The next notification that arrives will restart the app into the background, calling (as explained above) didFinishLaunchingWithOptions rather than didReceiveRemoteNotification. Note that you can’t test this scenario by swiping the app out of memory yourself to kill it, because then iOS will not launch it into the background in response to a notification arriving.

(Added 30 April, 2016) Alternatively, as pointed out by commenters, you can set the app to “Wait for executable to be launched” in the scheme Info tab before you run it on the device.

*Updated 30 April, 2016 to correctly describe the behavior when a notification arrives or a notification is tapped, and the app is not currently running.