Registering, Scheduling, and Handling User Notifications

The are several tasks that an iOS or OS X app should do to register, schedule, and handle both local and remote notifications.

Registering for Notification Types in iOS

In iOS 8 and later, apps that use either local or remote notifications must register the types of notifications they intend to deliver. The system then gives the user the ability to limit the types of notifications your app displays. The system does not badge icons, display alert messages, or play alert sounds if any of these notification types are not enabled for your app, even if they are specified in the notification payload.

In iOS, use the registerUserNotificationSettings: method of UIApplication to register notification types. The notification types represent the user interface elements the app displays when it receives a notification: badging the app’s icon, playing a sound, and displaying an alert. If you don’t register any notification types, the system pushes all remote notifications to your app silently, that is, without displaying any user interface. Listing 2-1 shows how an app registers notification types.

Listing 2-1  Registering notification types

UIUserNotificationType types = UIUserNotificationTypeBadge |
             UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
 
UIUserNotificationSettings *mySettings =
            [UIUserNotificationSettings settingsForTypes:types categories:nil];
 
[[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];

Notice the use of the categories parameter in Listing 2-1. A category is a group of actions that can be displayed in conjunction with a single notification. You can learn more about categories in Using Notification Actions in iOS.

The first time you call the registerUserNotificationSettings: method, iOS presents a dialog that asks the user for permission to present the types of notifications the app registered. After the user replies, iOS asynchronously calls back to the UIApplicationDelegate object with the application:didRegisterUserNotificationSettings: method, passing a UIUserNotificationType object that specifies the types of notifications the user allows.

Users can change their notification settings at any time using the Settings app. Your app is added to the Settings app as soon as you call registerUserNotificationSettings:. Users can enable or disable notifications, as well as modify where and how notifications are presented. Because the user can change their initial setting at any time, call currentUserNotificationSettings before you do any work preparing a notification for presentation.

In iOS 8 and later, calling registerUserNotificationSettings: applies to remote notifications as well as local notifications. Because doing so specifies the types of remote notifications the app displays, the registerForRemoteNotificationTypes: is deprecated in iOS 8. You can learn more about remote notifications in Registering for Remote Notifications.

Scheduling Local Notifications

In iOS, you create a UILocalNotification object and schedule its delivery using the scheduleLocalNotification: method of UIApplication. In OS X, you create an NSUserNotification object (which includes a delivery time) and the NSUserNotificationCenter is responsible for delivering it appropriately. (An OS X app can also adopt the NSUserNotificationCenterDelegate protocol to customize the behavior of the default NSUserNotificationCenter object.)

Creating and scheduling local notifications in iOS requires that you perform the following steps:

  1. In iOS 8 and later, register for notification types, as described in Registering for Notification Types in iOS. (In OS X and earlier versions of iOS, you need register only for remote notifications.) If you already registered notification types, call currentUserNotificationSettings to get the types of notifications the user accepts from your app.

  2. Allocate and initialize a UILocalNotification object.

  3. 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).

  4. As appropriate, configure an alert, icon badge, or sound so that the notification can be delivered to users according to their preferences. (To learn about when different notification types are appropriate, see Notifications.)

    • An alert has a property for the message (the alertBody property) and for the title of the action button or slider (alertAction). Specify strings that are localized for the user’s current language preference. If your notifications can be displayed on Apple Watch, assign a value to the alertTitle property.

    • To display a number in a badge on the app icon, use the applicationIconBadgeNumber property.

    • To play a sound, assign a sound to the soundName property. You can assign the filename of a nonlocalized custom sound in the app’s main bundle (or data container) or you can assign UILocalNotificationDefaultSoundName to get the default system sound. A sound should always accompany the display of an alert message or the badging of an icon; a sound should not be played in the absence of other notification types.

  5. Optionally, you can attach custom data to the notification through the userInfo property. For example, a notification that’s sent when a CloudKit record changes includes the identifier of the record, so that a handler can get the record and update it.

  6. Optionally, in iOS 8 and later, your local notification can present custom actions that your app can perform in response to user interaction, as described in Using Notification Actions in iOS.

  7. Schedule the local notification for delivery.

    You schedule a local notification by calling scheduleLocalNotification:. The app 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-2 creates and schedules a notification to inform the user of a hypothetical to-do list app about the impending due date of a to-do item. There are a couple things to note about it. For the alertBody, alertAction, and alertTitle 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-2  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.alertTitle = NSLocalizedString(@"Item Due", 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 app object, and you can cancel all scheduled notifications by calling cancelAllLocalNotifications. Both of these methods also programmatically dismiss a currently displayed notification alert. For example, you might want to cancel a notification that’s associated with a reminder the user no longer wants.

Apps 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, an app can present the notification immediately using the UIApplication method presentLocalNotificationNow: (iOS gives an app a limited time to run in the background).

In OS X, you might write code like that shown in Listing 2-3 to create a local notification and schedule it for delivery. Note that OS X doesn’t deliver a local notification if your app is currently frontmost. Also, OS X users can change their preferences for receiving notifications in System Preferences.

Listing 2-3  Creating and scheduling a local notification in OS X

//Create a new local notification
NSUserNotification *notification = [[NSUserNotification alloc] init];
//Set the title of the notification
notification.title = @"My Title";
//Set the text of the notification
notification.informativeText = @"My Text";
//Schedule the notification to be delivered 20 seconds after execution
notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:20];
 
//Get the default notification center and schedule delivery
[[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification];

Registering for Remote Notifications

An app must register with Apple Push Notification service (APNs) to receive remote notifications sent by the app’s push provider. In iOS 8 and later, registration has four stages:

  1. Register the notification types your app supports using registerUserNotificationSettings:.

  2. Register to receive push notifications via APNs by calling your app’s registerForRemoteNotifications method.

  3. Store the device token returned to the app delegate by the server for a successful registration, or handle registration failure gracefully.

  4. Forward the device token to the app’s push provider.

(In iOS 7, instead of the first two steps, you register by calling the registerForRemoteNotificationTypes: method of UIApplication, and in OS X by calling the registerForRemoteNotificationTypes: method of NSApplication.) The actions that take place during the registration sequence are illustrated by Figure 3-3 in Token Generation and Dispersal.

Device tokens can change, so your app needs to reregister every time it is launched.

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 app passes this token, encoded in binary format, to its provider. If there is a problem in obtaining the token, the operating system informs the delegate by calling the application:didFailToRegisterForRemoteNotificationsWithError: method (or the application:didFailToRegisterForRemoteNotificationsWithError: method in OS X). 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.

By requesting the device token and passing it to the provider every time your app launches, you ensure that the provider has the current token for the device. Otherwise, pushes may not make their way to the user's 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), the user must launch the app 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 app has previously registered, calling registerForRemoteNotifications 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-4 gives a simple example of how you might register for remote notifications in an iOS app. The code would be similar for a Mac app.

Listing 2-4  Registering for remote notifications

- (void)applicationDidFinishLaunching:(UIApplication *)app {
   // other setup tasks here....
    UIUserNotificationType types = UIUserNotificationTypeBadge |
                 UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
 
    UIUserNotificationSettings *mySettings =
                [UIUserNotificationSettings settingsForTypes:types categories:nil];
 
    [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
    [[UIApplication sharedApplication] registerForRemoteNotifications];
}
 
// 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);
}

In your application:didFailToRegisterForRemoteNotificationsWithError: implementation, you should process that error object appropriately and make sure you disable any logic within your app that depends on receiving remote notifications. You don't want do any unnecessary processing within your app for notifications that aren't going to be coming in. Just gracefully degrade.

Handling Local and Remote Notifications

Let’s review the possible scenarios that can arise when the system delivers a local notification or a remote notification for an app.

The notification is delivered when the app isn’t running in the foreground. In this case, the system presents the notification, displaying an alert, badging an icon, perhaps playing a sound, and perhaps displaying one or more action buttons for the user to tap.

The user taps a custom action button in an iOS 8 notification. In this case, iOS calls either application:handleActionWithIdentifier:forRemoteNotification:completionHandler: or application:handleActionWithIdentifier:forLocalNotification:completionHandler:. In both methods, you get the identifier of the action so that you can determine which button the user tapped. You also get either the remote or local notification object, so that you can retrieve any information you need to handle the action.

The user taps the default button in the alert or taps (or clicks) the app icon. If the default action button is tapped (on a device running iOS), the system launches the app and the app calls its delegate’s application:didFinishLaunchingWithOptions: method, passing in the notification payload (for remote notifications) or the local-notification object (for local notifications). Although application:didFinishLaunchingWithOptions: isn’t the best place to handle the notification, getting the payload at this point gives you the opportunity to start the update process before your handler method is called.

If the notification is remote, the system also calls application:didReceiveRemoteNotification:fetchCompletionHandler:.

If the app icon is clicked on a computer running OS X, the app calls the delegate’s applicationDidFinishLaunching: method in which the delegate can obtain the remote-notification payload. If the app icon is tapped on a device running iOS, the app calls the same method, but furnishes no information about the notification.

The notification is delivered when the app is running in the foreground. The app calls the UIApplicationDelegate method application:didReceiveLocalNotification: or application:didReceiveRemoteNotification:fetchCompletionHandler:. (If application:didReceiveRemoteNotification:fetchCompletionHandler: isn’t implemented, the system calls application:didReceiveRemoteNotification:.) In OS X, the system calls application:didReceiveRemoteNotification:.

An app 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 app in Listing 2-5 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 app’s initial context. As shown in this example, you might appropriately reset the badge number on the app icon—or remove it if there are no outstanding items—as part of handling the notification.

Listing 2-5  Handling a local notification when an app 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 app 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 your notification handling methods, the app delegate might perform a major additional task. Just after the app launches, the delegate should connect with its provider and fetch the waiting data.

The code in Listing 2-6 shows an implementation of the application:didReceiveLocalNotification: method which is called when app is running in the foreground. Here the app delegate does the same work as it does in Listing 2-5. 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 app 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 app to catch remote notifications that the system delivers while it is running in the foreground, the app delegate must implement the application:didReceiveRemoteNotification:fetchCompletionHandler: 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 app icon. 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 app’s current context.

Using Notification Actions in iOS

In OS X and iOS versions prior to iOS 8, user notifications can have only one default action. In iOS 8 and later, user notifications can have additional custom actions. Two actions can be displayed on the lock screen, in a banner, and in Notification Center. In modal alerts, notifications can display up to four actions when the user taps the Options button. To use notification actions in your app, you need to register the actions, schedule a local notification or push a remote notification, and handle the action chosen by the user.

Registering Notification Actions

To use notification actions in your app, you must define the actions, group them into categories, and then register them with your app’s shared UIApplication instance.

To define a notification action, first you must create and initialize an instance of a notification action class, typically UIMutableUserNotificationAction. Then you define an identifier, passed back to your app when it handles the action, and a localized string displayed to the user on the action button. Next, you set the action’s activation mode to foreground if the action needs to interrupt the user or background if not. Finally, you declare whether the action is destructive, meaning its button displays red, and whether choosing the action requires the user to enter their passcode. Listing 2-7 illustrates these steps.

Listing 2-7  Defining a notification action

UIMutableUserNotificationAction *acceptAction =
            [[UIMutableUserNotificationAction alloc] init];
 
// Define an ID string to be passed back to your app when you handle the action
acceptAction.identifier = @"ACCEPT_IDENTIFIER";
 
// Localized string displayed in the action button
acceptAction.title = @"Accept";
 
// If you need to show UI, choose foreground
acceptAction.activationMode = UIUserNotificationActivationModeBackground;
 
// Destructive actions display in red
acceptAction.destructive = NO;
 
// Set whether the action requires the user to authenticate
acceptAction.authenticationRequired = NO;

The activationMode property determines whether iOS launches your app in the foreground or background when the user responds to the notification. If you set it to UIUserNotificationActivationModeBackground, your app is given seconds to run. If the destructive property is NO, the action’s button appears blue; if it’s YES, the button is red. If you set the action’s authenticationRequired property to YES and the device is locked when the user responds to the notification, the user must enter a passcode when choosing the action. However, this does not unlock the device, so if your app needs to access files, make sure the files are in the right data protection class. When the value of the activationMode property is UIUserNotificationActivationModeForeground, the value of the authenticationRequired property is assumed to be YES regardless of its actual value.

For example, to configure actions for a calendar app, an Accept action needs no additional user interaction after the user taps the Accept button, so its activationMode can be background. Also, the Accept action is not destructive, so it doesn’t appear in red on the notification and lock screen, and it doesn’t need authentication because accepting an invitation is relatively harmless. As another example, a Trash action to delete a message in a Mail app also needs no further user interaction, so it can run in the background, but it is destructive, so its destructive property should be set to YES, and it requires authentication because you don’t want someone else deleting your messages. On the other hand, a Reply action requires user interaction, so the activation mode should be foreground. It’s not destructive, but the user must unlock the device because foreground actions always require authentication, regardless of the value in the authenticationRequired property.

After you have defined your actions, you need to group each of them into a category, which associates a type of notification with a set of related actions. For example, an Invite category could have Accept, Maybe, and Decline actions. A New mail category could have Mark as Read and Trash, and a Tagged category could have Like, Comment, and Untag actions. When crafting local or remote notifications for a user’s device, you specify the category that contains the actions you want to display with that notification. When the notification is displayed, iOS uses the category information to determine which buttons to display in the notification alert and to notify you of which action the user selected.

To group actions into a category, create and initialize an instance of a notification category class, typically UIMutableUserNotificationCategory. Then define an identifier for the category, which you include in local notifications and the push payload of remote notifications.

Next you add actions to the category and set their action context. There are two user notification action contexts: the default context, which supports four actions, and the minimal context, which displays two. The context relates to the part of the user interface in which the notification is presented—the lock screen only has room to display two actions, so the minimal context applies, whereas a modal alert has room for a full set of actions, and the default context applies. Listing 2-8 shows how these steps can be coded.

Listing 2-8  Grouping actions into categories

// First create the category
UIMutableUserNotificationCategory *inviteCategory =
        [[UIMutableUserNotificationCategory alloc] init];
 
// Identifier to include in your push payload and local notification
inviteCategory.identifier = @"INVITE_CATEGORY";
 
// Add the actions to the category and set the action context
[inviteCategory setActions:@[acceptAction, maybeAction, declineAction]
    forContext:UIUserNotificationActionContextDefault];
 
// Set the actions to present in a minimal context
[inviteCategory setActions:@[acceptAction, declineAction]
    forContext:UIUserNotificationActionContextMinimal];

The two setActions:forContext: messages in Listing 2-8 ensure that the actions are presented in the correct order in the default context, and that the most important actions are presented in a minimal context. That is, in a modal alert, the actions displayed are Accept, Maybe, and Decline (in that order), but on the lock screen the two actions displayed are Accept and Decline. If the second setActions:forContext: were not specified, only the first two actions of the default context would be displayed on the lock screen: Accept and Maybe.

After you define your notification action categories, you need to register them. You do this by grouping them together in a set, providing them to your user notification settings, and then registering those settings with your shared app instance. Listing 2-9 illustrates these steps.

Listing 2-9  Registering notification categories

NSSet *categories = [NSSet setWithObjects:inviteCategory, alarmCategory, ...
 
UIUserNotificationSettings *settings =
       [UIUserNotificationSettings settingsForTypes:types categories:categories];
 
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];

The UIUserNotificationSettings class method settingsForTypes:categories: method is the same one shown in Listing 2-1 which passed nil for the categories parameter, and the notification settings are registered in the same way with the app instance. In this case, the notification categories, as well as the notification types, are included in the app’s notification settings.

Pushing a Remote Notification or Scheduling a Local Notification with Custom Actions

To show the notification actions that you defined, categorized, and registered, you must push a remote notification or schedule a local notification. In the remote notification case, you need to include the category identifier in your push payload, as shown in Listing 2-10. Support for categories is a collaboration between your iOS app and your push notification server. When your push server wants to send a notification to a user, it can add a category key with an appropriate value to the notification’s payload. When iOS sees a push notification with a category key, it looks up the categories that were registered by the app. If iOS finds a match, it displays the corresponding actions with the notification.

In iOS 8, the previous size limit of 256 bytes for a push payload has been increased to 2 kilobytes. See The Notification Payload for details about the remote-notification payload.

Listing 2-10  Push payload including category identifier

{
    "aps" :  {
        "alert" : "You’re invited!",
        "category" : "INVITE_CATEGORY"
    }
}

In the case of a local notification, you create the notification as usual, then set the category of the actions to be presented, and finally, schedule the notification as usual, as shown in Listing 2-11.

Listing 2-11  Defining a category of actions for a local notification

UILocalNotification *notification = [[UILocalNotification alloc] init];
. . .
notification.category = @"INVITE_CATEGORY";
[[UIApplication sharedApplication] scheduleLocalNotification:notification];

Handling Notification Actions

If your app is not running in the foreground, to handle the default action when a user just swipes or taps on a notification, iOS launches your app in the foreground and calls the UIApplicationDelegate method application:didFinishLaunchingWithOptions: passing in the local notification or the remote notification in the options dictionary. In the remote notification case, the system also calls application:didReceiveRemoteNotification:fetchCompletionHandler:.

If your app is already in the foreground, iOS does not show the notification. Instead, to handle the default action, it calls one of the UIApplicationDelegate methods application:didReceiveLocalNotification: or application:didReceiveRemoteNotification:fetchCompletionHandler:. (If you don’t implement application:didReceiveRemoteNotification:fetchCompletionHandler:, iOS calls application:didReceiveRemoteNotification:.)

Finally, to handle the custom actions available in iOS 8, you need to implement at least one of two new methods on your app delegate, application:handleActionWithIdentifier:forRemoteNotification:completionHandler: or application:handleActionWithIdentifier:forLocalNotification:completionHandler:. In either case, you receive the action identifier, which you can use to determine what action was tapped. You also receive the notification, remote or local, which you can use to retrieve any information you need to handle that action. Finally, the system passes you the completion handler, which you must call when you finish handling the action. Listing 2-12 shows an example implementation that calls a self-defined action handler method.

Listing 2-12  Handling a custom notification action

- (void)application:(UIApplication *) application
              handleActionWithIdentifier: (NSString *) identifier
          // either forLocalNotification: (NSDictionary *) notification or
                   forRemoteNotification: (NSDictionary *) notification
                       completionHandler: (void (^)()) completionHandler {
 
    if ([identifier isEqualToString: @"ACCEPT_IDENTIFIER"]) {
        [self handleAcceptActionWithNotification:notification];
    }
 
    // Must be called when finished
    completionHandler();
}

Using Location-Based Notifications

In iOS 8 and later, you can send the user a notification whenever they arrive at a particular geographic location. This feature uses Core Location and is implemented through simple API additions to the UILocalNotification class. You define Core Location region objects and attach them to a notification so that the notification fires when the user comes near, enters, or exits a region. You can make it so that the notification is presented only the first time that the user enters this region, or you could have the notifications fire continuously if that makes sense for your app.

Registering for Location-Based Notifications

Before you can schedule a location-based notification, you must register with Core Location. To register, create a CLLocationManager instance and set your app as the delegate on this manager. The delegate receives callbacks that tell your app whether it is allowed to track the user's location. Finally, you must send the location manager instance a requestWhenInUseAuthorization message, as shown in Listing 2-13. The first time your app calls this method, it displays an alert that asks the user to allow or disallow your app’s tracking of the user’s whereabouts. In addition to asking the user for permission for your app to access their location, the alert also displays some explanatory text that you provide, such as “Enabling location tracking allows friends to see where you are.” This explanatory string is required to use location services. Your app defines the string in its Info.plist file under the NSLocationWhenInUseUsageDescription key. If your app runs in locales with different languages, make sure you localize the string appropriately in your Info.plist strings file. If the user agrees to allow access, your app can track the user's location when your app is running in the foreground.

Listing 2-13  Getting authorization for tracking the user’s location

CLLocationManager *locMan = [[CLLocationManager alloc] init];
// Set a delegate that receives callbacks that specify if your app is allowed to track the user's location
locMan.delegate = self;
 
// Request authorization to track the user’s location and enable location-based notifications
[locMan requestWhenInUseAuthorization];

Note that users may see location-based notification alerts even when your app is in the background or suspended. However, an app does not receive any callbacks until users interact with the alert and the app is allowed to access their location.

Handling Core Location Callbacks

At startup, you should check the authorization status and store the state information you need to allow or disallow location-based notifications. The first delegate callback from the Core Location manager that you must handle is locationManager:didChangeAuthorizationStatus:, which reports changes to the authorization status. First, check that the status passed with the callback is kCLAuthorizationStatusAuthorizedWhenInUse, as shown in Listing 2-14, meaning that your app is authorized to track the user’s location. Then you can begin scheduling location-based notifications.

Listing 2-14  Handling the Core Location authorization callback

- (void)locationManager:(CLLocationManager *)manager
               didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
 
    // Check status to see if the app is authorized
    BOOL canUseLocationNotifications = (status == kCLAuthorizationStatusAuthorizedWhenInUse);
 
    if (canUseLocationNotifications) {
        [self startShowingLocationNotifications]; // Custom method defined below
    }
}

Listing 2-15 shows how to schedule a notification that triggers when the user enters a region. The first thing you must do, as with a local notification triggered by a date or a time, is to create an instance of UILocalNotification and define its type, in this case an alert.

Listing 2-15  Scheduling a location-based notification

- (void)startShowingNotifications {
 
    UILocalNotification *locNotification = [[UILocalNotification alloc] init];
    locNotification.alertBody = @“You have arrived!”;
    locNotification.regionTriggersOnce = YES;
 
    locNotification.region = [[CLCircularRegion alloc]
                        initWithCenter:LOC_COORDINATE
                                radius:LOC_RADIUS
                            identifier:LOC_IDENTIFIER];
 
    [[UIApplication sharedApplication] scheduleLocalNotification:locNotification];
}

When the user enters the region defined in Listing 2-15, assuming the app isn't running in the foreground, the app displays an alert saying: “You have arrived!" The next line specifies that this notification triggers only once, the first time the user enters or exits this region. This is actually the default behavior, so it's superfluous to specify YES, but you could set this property to NO if that makes sense for your users and for your app.

Next, you create a CLCircularRegion instance and set it on the region property of the UILocalNotification instance. In this case we're giving it an app-defined location coordinate with some radius so that when the user enters this circle, this notification is triggered. This example uses a CLCircularRegion property, but you could also use CLBeaconRegion or any other type of CLRegion subclass.

Finally, call scheduleLocalNotification: on your UIApplication shared instance, passing this notification just like you would do for any other local user notification.

Handling Location-Based Notifications

Assuming that your app is suspended when the user enters the region defined in Listing 2-15, an alert is displayed that says: "You have arrived." Your app can handle that local notification in the application:didFinishLaunchingWithOptions: app delegate method callback. Alternatively, if your app is executing in the foreground when the user enters that region, your app delegate is called back with application:didReceiveLocalNotification: message.

The logic for handling a location-based notification is very similar for both the application:didFinishLaunchingWithOptions: and application:didReceiveLocalNotification: methods. Both methods provide the notification, an instance of UILocalNotification, which has a region property. If that property is not nil, then the notification is a location-based notification, and you can do whatever makes sense for your app. The example code in Listing 2-16 calls a hypothetical method of the app delegate named tellFriendsUserArrivedAtRegion:.

Listing 2-16  Handling a location-based notification

- (void)application:(UIApplication *)application
                         didReceiveLocalNotification: (UILocalNotification *)notification {
 
    CLRegion *region = notification.region;
 
    if (region) {
           [self tellFriendsUserArrivedAtRegion:region];
    }
}

Finally, remember that the application:didReceiveLocalNotification: method is not called if the user disables Core Location, which they can do at any time in the Settings app under Privacy > Location Services.

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 app. The sound files can be in the main bundle of the client app or in the Library/Sounds folder of the app’s data container.

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 app bundle or to the Library/Sounds folder of your data container.

You can 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 app:

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.

Passing the Provider the Current Language Preference (Remote Notifications)

If an app 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 app.) The client app 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-17 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-17  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 app 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 app supports, the provider should localize the message text in a widely spoken fallback language such as English or Spanish.