This chapter describes the tasks that a iPhone, iPad, or iPod touch 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.
Because custom alert sounds are played by the iOS system-sound facility, 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:
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];
|
[dateComps release];
|
|
UILocalNotification *localNotif = [[UILocalNotification alloc] init];
|
if (localNotif == nil)
|
return;
|
localNotif.fireDate = [itemDate addTimeInterval:-(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];
|
[localNotif release];
|
}
|
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 instance variable
|
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:
Note: Unless otherwise noted, all methods cited(引入) in this section are declared(声明) with identical(相同的) signatures by both UIApplication and NSApplication, and, for delegates, by both NSApplicationDelegate Protocol and UIApplicationDelegate.
What happens between the application, the device, Apple Push Notification Service, and the provider during this sequence is illustrated by Figure 3-3 in “Token Generation and Dispersal.”
An application should register every time it launches and give its provider the current token. It calls theregisterForRemoteNotificationTypes: method to kick off(揭开序幕) the registration process. The parameter of this method takes a UIRemoteNotificationType (or, for Mac OS X, a NSRemoteNotificationType) bit mask that specifies the initial types of notifications that the application wishes to receive—for example, icon-badging and sounds, but not alert messages. In iOS, users can thereafter(其后) modify the enabled notification types in the Notifications preference of the Settings application. In both iOS and Mac OS X, you can retrieve the currently enabled notification types by calling the enabledRemoteNotificationTypes method. The operating system does not badge icons, display alert messages, or play alert sounds if any of these notifications types are not enabled, even if they are specified in the notification payload.
Mac OS X Note: Because the only notification type supported for non-running applications is icon-badging, simply pass NSRemoteNotificationTypeBadge as the parameter of registerForRemoteNotificationTypes:.
If registration is successful, APNs returns a device token to the device and iOS passes the token to the application delegate in the application:didRegisterForRemoteNotificationsWithDeviceToken: method. The application should connect 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.)
iOS Note: If a cellular or Wi-Fi connection is not available, neither the application:didRegisterForRemoteNotificationsWithDeviceToken: method or the application:didFailToRegisterForRemoteNotificationsWithError: method is called. For Wi-Fi connections, this sometimes occurs when the device cannot connect with APNs over port 5223. If this happens, the user can move to another Wi-Fi network that isn’t blocking this port or, on an iPhone or iPad, wait until the cellular data service becomes available. In either case, the connection should then succeed and one of the delegation methods is called.
By requesting(请求) the device token and passing it to the provider every time your application launches, you help to 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(开销).
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 OS X application. (SendProviderDeviceToken is a hypothetical method defined by the client in which it connects with its provider and passes it the device token.)
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 operating delivers a local notification or a remote notification for an application.
Note: The delegate methods cited in this section that have “RemoteNotification” in their name are declared with identical signatures by by both NSApplicationDelegate Protocol and UIApplicationDelegate.
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:
iOS Note: In iOS, you can determine whether an application is launched as a result of the user tapping the action button or whether the notification was delivered to the already-running application by examining the application state. In the delegate’s implementation of the application:didReceiveRemoteNotification: or application:didReceiveLocalNotification: method, get the value of the applicationState property and evaluate(评价、评估) it. If the value is UIApplicationStateInactive, the user tapped the action button; if the value is UIApplicationStateActive, the application was frontmost(最前面的) when it received the notification.
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
|
application.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.
Important You should never define custom properties in the notification payload for the purpose of transporting customer data or any other sensitive data. Delivery of remote notifications is not guaranteed. One example of an appropriate usage for a custom payload property is a string identifying an email account from which messages are downloaded to an email client; the application can incorporate this string in its download user-interface. Another example of custom payload property is a timestamp for when the provider first sent the notification; the client application can use this value to gauge(估计) how old the notification is.
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....
|
}
|
Note: A client application should always communicate with its provider asynchronously or on a secondary thread.
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
|
application.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 Mac 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).
iOS Note: If the user unlocks the device shortly after a remote-notification alert is displayed, the operating system automatically triggers the action associated with the alert. (This behavior is consistent with SMS and calendar alerts.) This makes it even more important that actions related to remote notifications do not have destructive consequences. A user should always make decisions that result in the destruction of data in the context of the application that stores the data.
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”.
Note: For more information about the loc-key and loc-args properties and client-side message localizations, see “The Notification Payload.”
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 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.