Scheduling, Registering, and Handling Notifications

This chapter describes the tasks that an iOS on OS X application should (or might) do to schedule local notifications, register remote notifications, and handle both local and remote notifications. Because the client-side API for push notifications refers to push notifications as remote notifications, that terminology is used in this chapter.

Preparing Custom Alert Sounds

For remote notifications in iOS, you can specify a custom sound that iOS plays when it presents a local or remote notification for an application. The sound files must be in the main bundle of the client application.

Custom alert sounds are played by the iOS system-sound facility, so they must be in one of the following audio data formats:

You can package the audio data in an aiff, wav, or caf file. Then, in Xcode, add the sound file to your project as a nonlocalized resource of the application bundle.

You may use the afconvert tool to convert sounds. For example, to convert the 16-bit linear PCM system sound Submarine.aiff to IMA4 audio in a CAF file, use the following command in the Terminal application:

afconvert /System/Library/Sounds/Submarine.aiff ~/Desktop/sub.caf -d ima4 -f caff -v

You can inspect a sound to determine its data format by opening it in QuickTime Player and choosing Show Movie Inspector from the Movie menu.

Custom sounds must be under 30 seconds when played. If a custom sound is over that limit, the default system sound is played instead.

Scheduling Local Notifications

Creating and scheduling local notifications in iOS requires that you perform a few simple steps:

  1. Allocate and initialize a UILocalNotification object.

  2. Set the date and time that the operating system should deliver the notification. This is the fireDate property.

    If you set the timeZone property to the NSTimeZone object for the current locale, the system automatically adjusts the fire date when the device travels across (and is reset for) different time zones. (Time zones affect the values of date components—that is, day, month, hour, year, and minute—that the system calculates for a given calendar and date value.) You can also schedule the notification for delivery on a recurring basis (daily, weekly, monthly, and so on).

  3. Configure the substance of the notification: alert, icon badge number, and sound.

    • The alert has a property for the message (the alertBody property) and for the title of the action button or slider (alertAction); both of these string values can be internationalized for the user’s current language preference.

    • You set the badge number to display on the application icon through the applicationIconBadgeNumber property.

    • You can assign the filename of a nonlocalized custom sound in the application’s main bundle to the soundName property; to get the default system sound, assign UILocalNotificationDefaultSoundName. Sounds should always accompany an alert message or icon badging; they should not be played otherwise.

  4. Optionally, you can attach custom data to the notification through the userInfo property.

    Keys and values in the userInfo dictionary must be property-list objects.

  5. Schedule the local notification for delivery.

    You schedule a local notification by calling the UIApplication method scheduleLocalNotification:. The application uses the fire date specified in the UILocalNotification object for the moment of delivery. Alternatively, you can present the notification immediately by calling the presentLocalNotificationNow: method.

The method in Listing 2-1 creates and schedules a notification to inform the user of a hypothetical to-do list application about the impending due date of a to-do item. There are a couple things to note about it. For the alertBody and alertAction properties, it fetches from the main bundle (via the NSLocalizedString macro) strings localized to the user’s preferred language. It also adds the name of the relevant to-do item to a dictionary assigned to the userInfo property.

Listing 2-1  Creating, configuring, and scheduling a local notification

- (void)scheduleNotificationWithItem:(ToDoItem *)item interval:(int)minutesBefore {
    NSCalendar *calendar = [NSCalendar autoupdatingCurrentCalendar];
    NSDateComponents *dateComps = [[NSDateComponents alloc] init];
    [dateComps setDay:item.day];
    [dateComps setMonth:item.month];
    [dateComps setYear:item.year];
    [dateComps setHour:item.hour];
    [dateComps setMinute:item.minute];
    NSDate *itemDate = [calendar dateFromComponents:dateComps];
 
    UILocalNotification *localNotif = [[UILocalNotification alloc] init];
    if (localNotif == nil)
        return;
    localNotif.fireDate = [itemDate dateByAddingTimeIntervalInterval:-(minutesBefore*60)];
    localNotif.timeZone = [NSTimeZone defaultTimeZone];
 
    localNotif.alertBody = [NSString stringWithFormat:NSLocalizedString(@"%@ in %i minutes.", nil),
         item.eventName, minutesBefore];
    localNotif.alertAction = NSLocalizedString(@"View Details", nil);
 
    localNotif.soundName = UILocalNotificationDefaultSoundName;
    localNotif.applicationIconBadgeNumber = 1;
 
    NSDictionary *infoDict = [NSDictionary dictionaryWithObject:item.eventName forKey:ToDoItemKey];
    localNotif.userInfo = infoDict;
 
    [[UIApplication sharedApplication] scheduleLocalNotification:localNotif];
}

You can cancel a specific scheduled notification by calling cancelLocalNotification: on the application object, and you can cancel all scheduled notifications by calling cancelAllLocalNotifications. Both of these methods also programmatically dismiss a currently displayed notification alert.

Applications might also find local notifications useful when they run in the background and some message, data, or other item arrives that might be of interest to the user. In this case, they should present the notification immediately using the UIApplication method presentLocalNotificationNow: (iOS gives an application a limited time to run in the background). Listing 2-2 illustrates how you might do this.

Listing 2-2  Presenting a local notification immediately while running in the background

 
- (void)applicationDidEnterBackground:(UIApplication *)application {
    NSLog(@"Application entered background state.");
    // bgTask is a property of the class
    NSAssert(self.bgTask == UIInvalidBackgroundTask, nil);
 
    bgTask = [application beginBackgroundTaskWithExpirationHandler: ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [application endBackgroundTask:self.bgTask];
            self.bgTask = UIInvalidBackgroundTask;
        });
    }];
 
    dispatch_async(dispatch_get_main_queue(), ^{
        while ([application backgroundTimeRemaining] > 1.0) {
            NSString *friend = [self checkForIncomingChat];
            if (friend) {
                UILocalNotification *localNotif = [[UILocalNotification alloc] init];
                if (localNotif) {
                    localNotif.alertBody = [NSString stringWithFormat:
                        NSLocalizedString(@"%@ has a message for you.", nil), friend];
                    localNotif.alertAction = NSLocalizedString(@"Read Message", nil);
                    localNotif.soundName = @"alarmsound.caf";
                    localNotif.applicationIconBadgeNumber = 1;
                    [application presentLocalNotificationNow:localNotif];
                    [localNotif release];
                    friend = nil;
                    break;
                }
            }
        }
        [application endBackgroundTask:self.bgTask];
        self.bgTask = UIInvalidBackgroundTask;
    });
 
}

Registering for Remote Notifications

An application must register with Apple Push Notification service for the operating systems on a device and on a computer to receive remote notifications sent by the application’s provider. Registration has three stages:

  1. The app registers for remote notifications.

  2. The system sets up remote notifications for the app and, if registration is successful, passes a device token to the app delegate.

  3. The app sends its device token to the push provider.

The actions that take place during this sequence are illustrated by Figure 3-3 in “Token Generation and Dispersal.”

Device tokens can change. Your app needs to reregister every time it is launched—in iOS by calling the registerForRemoteNotificationTypes: method of UIApplication, and in OS X by calling the registerForRemoteNotificationTypes: method of NSApplication. The parameter passed to this method specifies the initial types of notifications that the application wishes to receive. Users can modify the enabled notification types at any point, using Settings in iOS or System Preferences in OS X. You can query the currently enabled notification types using the enabledRemoteNotificationTypes property of UIApplication or the enabledRemoteNotificationTypes property of NSApplication. The system does not badge icons, display alert messages, or play alert sounds if any of these notifications types are not enabled for your app, even if they are specified in the notification payload.

If registration is successful, APNs returns a device token to the device and iOS passes the token to the app delegate in the application:didRegisterForRemoteNotificationsWithDeviceToken: method. The application connects with its provider and pass it this token, encoded in binary format. If there is a problem in obtaining the token, the operating system informs the delegate by calling the application:didFailToRegisterForRemoteNotificationsWithError: method. The NSError object passed into this method clearly describes the cause of the error. The error might be, for instance, an erroneous aps-environment value in the provisioning profile. You should view the error as a transient state and not attempt to parse it. (See “Creating and Installing the Provisioning Profile” for details.)

By requesting the device token and passing it to the provider every time your application launches, you ensure that the provider has the current token for the device. If a user restores a backup to a device or computer other than the one that the backup was created for (for example, the user migrates data to a new device or computer), he or she must launch the application at least once for it to receive notifications again. If the user restores backup data to a new device or computer, or reinstalls the operating system, the device token changes. Moreover, never cache a device token and give that to your provider; always get the token from the system whenever you need it. If your application has previously registered, calling registerForRemoteNotificationTypes: results in the operating system passing the device token to the delegate immediately without incurring additional overhead. Also note that the delegate method may be called any time the device token changes, not just in response to your app registering or re-registering.

Listing 2-3 gives a simple example of how you might register for remote notifications in an iOS application. The code would be nearly identical for a Mac app.

Listing 2-3  Registering for remote notifications

- (void)applicationDidFinishLaunching:(UIApplication *)app {
   // other setup tasks here....
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];
}
 
// Delegation methods
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
    const void *devTokenBytes = [devToken bytes];
    self.registered = YES;
    [self sendProviderDeviceToken:devTokenBytes]; // custom method
}
 
- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
    NSLog(@"Error in registration. Error: %@", err);
}

Handling Local and Remote Notifications

Let’s review the possible scenarios when the system delivers a local notification or a remote notification for an application.

An application can use the passed-in remote-notification payload or, in iOS, the UILocalNotification object to help set the context for processing the item related to the notification. Ideally, the delegate does the following on each platform to handle the delivery of remote and local notifications in all situations:

The delegate for an iOS application in Listing 2-4 implements the application:didFinishLaunchingWithOptions: method to handle a local notification. It gets the associated UILocalNotification object from the launch-options dictionary using the UIApplicationLaunchOptionsLocalNotificationKey key. From the UILocalNotification object’s userInfo dictionary, it accesses the to-do item that is the reason for the notification and uses it to set the application’s initial context. As shown in this example, you should appropriately reset the badge number on the application icon—or remove it if there are no outstanding items—as part of handling the notification.

Listing 2-4  Handling a local notification when an application is launched

- (BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    UILocalNotification *localNotif =
        [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
    if (localNotif) {
        NSString *itemName = [localNotif.userInfo objectForKey:ToDoItemKey];
        [viewController displayItem:itemName];  // custom method
        app.applicationIconBadgeNumber = localNotif.applicationIconBadgeNumber-1;
    }
    [window addSubview:viewController.view];
    [window makeKeyAndVisible];
    return YES;
}

The implementation for a remote notification would be similar, except that you would use a specially declared constant in each platform as a key to access the notification payload:

The payload itself is an NSDictionary object that contains the elements of the notification—alert message, badge number, sound, and so on. It can also contain custom data the application can use to provide context when setting up the initial user interface. See “The Notification Payload” for details about the remote-notification payload.

When handling remote notifications in application:didFinishLaunchingWithOptions: or applicationDidFinishLaunching:, the application delegate might perform a major additional task. Just after the application launches, the delegate should connect with its provider and fetch the waiting data. Listing 2-5 gives a schematic illustration of this procedure.

Listing 2-5  Downloading data from a provider

- (void)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)opts {
    // check launchOptions for notification payload and custom data, set UI context
    [self startDownloadingDataFromProvider];  // custom method
    app.applicationIconBadgeNumber = 0;
    // other setup tasks here....
}

The code in Listing 2-6 shows an implementation of the application:didReceiveLocalNotification: method which, as you’ll recall, is called when application is running in the foreground. Here the application delegate does the same work as it does in Listing 2-4. It can access the UILocalNotification object directly this time because this object is an argument of the method.

Listing 2-6  Handling a local notification when an application is already running

- (void)application:(UIApplication *)app didReceiveLocalNotification:(UILocalNotification *)notif {
    NSString *itemName = [notif.userInfo objectForKey:ToDoItemKey];
    [viewController displayItem:itemName];  // custom method
    app.applicationIconBadgeNumber = notification.applicationIconBadgeNumber - 1;
}

If you want your application to catch remote notifications that the system delivers while it is running in the foreground, the application delegate should implement the application:didReceiveRemoteNotification: method. The delegate should begin the procedure for downloading the waiting data, message, or other item and, after this concludes, it should remove the badge from the application icon. (If your application frequently checks with its provider for new data, implementing this method might not be necessary.) The dictionary passed in the second parameter of this method is the notification payload; you should not use any custom properties it contains to alter your application’s current context.

Even though the only supported notification type for nonrunning applications in OS X is icon-badging, the delegate can implement application:didReceiveRemoteNotification: to examine the notification payload for other types of notifications and handle them appropriately (that is, display an alert or play a sound).

Passing the Provider the Current Language Preference (Remote Notifications)

If an application doesn’t use the loc-key and loc-args properties of the aps dictionary for client-side fetching of localized alert messages, the provider needs to localize the text of alert messages it puts in the notification payload. To do this, however, the provider needs to know the language that the device user has selected as the preferred language. (The user sets this preference in the General > International > Language view of the Settings application.) The client application should send its provider an identifier of the preferred language; this could be a canonicalized IETF BCP 47 language identifier such as “en” or “fr”.

Listing 2-7 illustrates a technique for obtaining the currently selected language and communicating it to the provider. In iOS, the array returned by the preferredLanguages property of NSLocale contains one object: an NSString object encapsulating the language code identifying the preferred language. The UTF8String coverts the string object to a C string encoded as UTF8.

Listing 2-7  Getting the current supported language and sending it to the provider

NSString *preferredLang = [[NSLocale preferredLanguages] objectAtIndex:0];
const char *langStr = [preferredLang UTF8String];
[self sendProviderCurrentLanguage:langStr]; // custom method
}

The application might send its provider the preferred language every time the user changes something in the current locale. To do this, you can listen for the notification named NSCurrentLocaleDidChangeNotification and, in your notification-handling method, get the code identifying the preferred language and send that to your provider.

If the preferred language is not one the application supports, the provider should localize the message text in a widely spoken fallback language such as English or Spanish.