通知那些事儿(三):注册、调度及处理用户通知

本文译自《Registering, Scheduling, and Handling User Notifications

应用在能接受本地通知和远程通知之前必须被正确的配置。这些配置在iOS和OS X中略有区别,但是其基本原则是一致的。在启动时,你的应用需要注册通知,并且在系统设置中正确设置通知选项。一旦注册完成,你就可以创建通知并推送到你的应用。而且应用将处理这些进来的通知,并作出合理的响应。

在iOS和tvOS中,注册分为两步:注册通知交互类型和注册通知本身。注册通知类型告诉了操作系统如何在通知来到时提醒用户。这个是本地通知和远程通知都需要的一个步骤。对于远程通知,你还必须注册第二步,就是获取应用特定的device token,APNs服务器会根据device token发送通知(本地通知无需该步)。在OS X系统中,注册通知是必须的,因为仅支持远程通知。

关于发送与接受远程通知更多的issue,参考Technical Note TN2265

注册通知类型

在iOS8.0系统之后,不管是本地通知还是远程通知都必须注册通知类型。应用可以通过显示应用角标弹出提醒或者播放提示音来提醒用户。当你请求任何一种通知类型时,系统会检查用户是否允许你的使用该类型进行通知(设置->通知->你的应用)。如果用户不允许使用某一类型的通知,系统就会忽略该类型。比如,一个通知想要用提示消息和提示音来展示,而用户不允许播放提示音,那么系统就只会显示提示消息。

通过UIApplicationshared单例对象的方法registerUserNotificationSettings:来注册你的App要支持的通知类型。利用Settings对象来指定你的应用是使用角标提示消息还是提示音。如果你未注册任何类型那么系统会静默推送所有的通知。下面代码演示了如何注册通知类型:

UIUserNotificationType types = (UIUserNotificationType) (UIUserNotificationTypeBadge |
             UIUserNotificationTypeSound | UIUserNotificationTypeAlert);

UIUserNotificationSettings *mySettings =
            [UIUserNotificationSettings settingsForTypes:types categories:nil];

[[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];

更通用的方法:

if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)
{
    [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound |UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
}
else
{
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound |UIRemoteNotificationTypeAlert)];
}

除了注册通知类型,应用还可以注册一个或多个通知分类。本地通知和远程通知都支持分类,你可以通过分类来区分通知的目的。你的iOS应用可以利用分类来对通知进行不同的处理。在watchOS中,分类也用来自定义呈现给用户的通知界面。

从iOS8.0开始,你可以针对某一类型的通知注册自定义的交互动作来创建交互式通知。当交互式通知到来时,系统会为每个注册了交互动作的通知创建按钮并在通知界面上显示出来。这些操作按钮为用户操作该通知提供了一个快速入口。比如,一个会议的通知提供了接受会议拒绝会议的操作。当用户点击了操作按钮,系统就会通知你的应用,给你一个执行该按钮对应任务的机会。这里是关于注册交互式通知的更多信息。

应用第一次调用registerUserNotificationSettings:方法时,iOS会提醒用户是否允许应用发送通知。在之后的启动中,调用该方法就不会提醒用户了。在调用该方法之后,iOS会异步的将结果返回给应用代理的application:didRegisterUserNotificationSettings:方法。第一次注册通知设置时,iOS会等待用户响应后才调用该方法,但是在之后的启动中,调用时会直接返回已存在的用户通知设置。

用户可以在设置应用中随时改变应用的通知设置。一旦应用的通知设置改变了,会在应用启动时调用registerUserNotificationSettings:,并且在application:didRegisterUserNotificationSettings:里获得注册通知设置的结果。所以,假如用户禁止了某些通知类型,那么就要避免在本地通知和远程通知中通知类型的配置了。

注册远程通知

一个应用想要接受远程通知就必须要在APNs上注册获得正确的device token。在iOS8.0之后,注册会涉及以下步骤:

  1. 注册你的应用需要支持的通知类型。(上节-注册通知类型);
  2. 调用方法registerForRemoteNotifications为你的应用注册远程通知。(在OS X中,你可以调用registerForRemoteNotificationTypes:一步完成注册通知类型和远程通知);
  3. 在应用代理方法application:didRegisterForRemoteNotificationsWithDeviceToken:中获取需要分发远程通知用到的device token。在application:didFailToRegisterForRemoteNotificationsWithError: 处理错误;
  4. 如果注册成功,将device token发送给服务器用来生成远程通知。

iOS需要注意的地方:在移动数据或者Wi-Fi连接都不可用的时候,方法application:didRegisterForRemoteNotificationsWithDeviceToken:application:didFailToRegisterForRemoteNotificationsWithError:都不会被调用。在Wi-Fi连接下,在设备不能连接到APNs指定端口时,也会发生这种情况。此时,用户需要切换到其他没有阻止该端口的Wi-Fi网络下。对于iPhone或iPad,等待直到数据连接可用。在上面两种方法处理中,设备应该能建立连接,并且会调用其中一个代理方法。

device token是针对特定设备上你的应用进行推送通知的钥匙。device token的改变后,需要在每次启动应用后将注册远程通知后获取到的token发送给你的服务器。device token会在用户新设备或电脑上上恢复备份数据或者重新安装操作系统时发生改变。当用户迁移数据到新设备或新电脑后,用户必须登录一次你的应用,否则远程通知是不能推送到该设备上的。

不要试图缓存device token。最好在需要的时候都从系统获取token。如果你的应用之前注册过远程推送,再次调用registerForRemoteNotifications方法并不会导致任何附加的开销,iOS会返回已经存在的device token给应用代理。另外,iOS一旦发现device token改变,就会调用应用代理中的方法,而不仅仅只是在注册或者重新注册通知的时候调用。

下面演示了如何在iOS中注册远程通知:

- (void)applicationDidFinishLaunching:(UIApplication *)app {
   // other setup tasks here....

   // Register the supported interaction types.
    UIUserNotificationType types = UIUserNotificationTypeBadge |
                 UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
    UIUserNotificationSettings *mySettings =
                [UIUserNotificationSettings settingsForTypes:types categories:nil];
    [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];

   // Register for remote notifications.
    [[UIApplication sharedApplication] registerForRemoteNotifications];
}

// Handle remote notification registration.
- (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);
}

application:didFailToRegisterForRemoteNotificationsWithError:实现中,你需要正确处理错误并且禁用任何和远程通知有关的特性。因为你将收不到任何通知,常用的方式就是做降级处理来避免许多与处理通知或显示通知等不必要的工作。

注册交互式通知类型

交互式通知让你可以在标准的iOS界面上为本地通知远程通知自定义操作按钮。交互式通知为用户提供了一种快捷的处理通知的方式。在iOS8之前,用户通知只有一种默认的处理操作。在iOS8之后,在锁屏界面、通知横幅以及通知中心的通知入口都能够显示一个或两个自定义的操作按钮。模态提醒(modal alert)可以达到四个。当用户选中了自定义操作按钮,iOS可以通知你的应用执行与这个操作按钮相关联的任务。

注:OS X不支持交互式通知。

定义你的交互式通知

交互式通知的配置依赖于一个或多个通知分类。每个分类代表了你应用中可能会收到的一种通知类型,以及为该定义的类型的通知的响应处理。对于每个分类,你必须定义好用户在接受到该类型通知的时候可能采取的操作。在iOS中,注册你的通知分类和操作也是和注册通知类型调用相同的方法registerUserNotificationSettings:

每个自定义操作必须包含一个按钮的标题以及当操作被选中的时候需要iOS传递给应用的信息。创建操作也就是创建一个UIMutableUserNotificationAction实例对象,并且配置好正确的属性即可。下面代码就创建了一个“accept”的操作:

UIMutableUserNotificationAction *acceptAction =
            [[UIMutableUserNotificationAction alloc] init];

// The identifier that you use internally to handle the action.
acceptAction.identifier = @"ACCEPT_IDENTIFIER";

// The localized title of the action button.
acceptAction.title = @"Accept";

// Specifies whether the app must be in the foreground to perform the action.
acceptAction.activationMode = UIUserNotificationActivationModeBackground;

// Destructive actions are highlighted appropriately to indicate their nature.
acceptAction.destructive = NO;

// Indicates whether user authentication is required to perform the action.
acceptAction.authenticationRequired = NO;

当你创建好任何操作对象,需要将这些操作对象赋给用来在你的应用中定义分类的UIUserNotificationCategory对象。如果是在启动的时候设定分类,一般创建UIMutableUserNotificationCategory类的实例。为新创建的实例赋值一个identifier并通过方法setActions:forContext:将操作与分类关联起来。content参数可以为不同的操作集合指定不同的系统界面。default content支持四个操作并且以模态显示消息。minimal content只支持两个操作,用于锁屏界面、消息横幅以及消息中心。

下面代码演示了如何创建一个“invitation category”,包含“接受”操作以及“可能”操作、“拒绝”操作:

// First create the category
UIMutableUserNotificationCategory *inviteCategory =
        [[UIMutableUserNotificationCategory alloc] init];

// Identifier to include in your push payload and local notification
inviteCategory.identifier = @"INVITE_CATEGORY";

// Set the actions to display in the default context
[inviteCategory setActions:@[acceptAction, maybeAction, declineAction]
    forContext:UIUserNotificationActionContextDefault];

// Set the actions to display in a minimal context
[inviteCategory setActions:@[acceptAction, declineAction]
    forContext:UIUserNotificationActionContextMinimal];

创建完通知操作的分类后,你就可以注册了,但是需要注意的是,每个分类标识必须是唯一的:

NSSet *categories = [NSSet setWithObjects:inviteCategory, alarmCategory, ...

UIUserNotificationSettings *settings =
       [UIUserNotificationSettings settingsForTypes:types categories:categories];

[[UIApplication sharedApplication] registerUserNotificationSettings:settings];

可以看到,注册的过程和注册通知类型是一致的,只是在注册通知类型中的categories参数从nil变为实际的categories

调度交互式通知

为了显示经过定义、分配分类和注册的通知操作,你必须推送一个远程通知或者调度安排一个本地通知。在远程通知的情况下,你需要在推送的payload中包含分类标识,像下面代码演示:

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

分类的支持需要iOS应用和服务器的协同。当你推送服务在发送一个通知给用户时,可以在通知的payload中添加一个category的键,并传递合适的值。当iOS在收到通知后,如果发现category的键,就会在应用中查找已经注册过的分类,如果匹配到,就会显示该通知的操作。

在本地通知情况下,照例创建一个通知,然后设置需要展示操作对应的分类,最后,调度安排通知的时间即可,如下:

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

处理交互式通知

如果你的应用不在前台运行,当用户只是点击或触摸通知的时候,iOS会启动你的应用在前台显示,然后调用应用代理方法application:didFinishLaunchingWithOptions:,此时,将本地通知或远程通知通过options字典传递到该方法中。在远程通知时,也会调用代理方法application:didReceiveRemoteNotification:fetchCompletionHandler:

如果你的应用已经在前台了,iOS不会显示通知。取而代之的是处理默认操作是会调用应用代理中的application:didReceiveLocalNotification:或者application:didReceiveRemoteNotification:fetchCompletionHandler:方法(如果你没有实现该方法application:didReceiveRemoteNotification:fetchCompletionHandler:,iOS会调用application:didReceiveRemoteNotification:)。

最后需要注意的是,只要在iOS8或最新的操作系统才支持自定义操作,你需要在你的应用代理中至少实现下面两个方法中的一个,分别是application:handleActionWithIdentifier:forRemoteNotification:completionHandler:application:handleActionWithIdentifier:forLocalNotification:completionHandler:。不管哪种方法,你都能通过操作标识来区分出哪个操作被点击了。你也可以收到不管是本地还是远程通知中需要处理该操作的所有信息。最后,系统会传给你一个completion hanlder,你需要在处理完操作之后调用它。下面代码演示了这个过程:

- (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();
}

调度本地通知

在iOS中,创建一个本地通知对象是用UILocalNotification类,然后调用应用代理的scheduleLocalNotification:方法来安排通知。在OS X中,利用NSUserNotification创建(它包含通知触发时间),和用NSUserNotificationCenter来分发通知。(同样也可以采取实现协议NSUserNotificationCenterDelegate中来自定义NSUserNotificationCenter对象的表现)。

在iOS中,创建并调度安排一个通知,你需要执行以下步骤:

  1. 在iOS8及之后的系统中,按本文开头的流程注册通知类型(在OS X和iOS之前版本中,只有远程通知需要注册通知类型)。如果你已经注册了通知类型,你可以通过调用方法NSUserNotificationCenter来获取用户所能接收到的通知类型;
  2. 分配内存和初始化UILocalNotification对象;
  3. 设置系统发送通知的日期和时间,即设置fireDate属性。

    如果你为当前区域设置了NSTimeZone对象的实例timeZone属性,系统将会在跨越不同时区时自动转换发送时间。

    你也可以安排通知以基于循环发送(比如每天、每周、每月等)。

  4. 根据用户喜好,配置好提示、角标或者提示音等随通知发送给用户。(可以在Notifications学到更多关于什么时候采取何种通知类型是合适的);

    • 提示包括一个消息属性(alertBody属性)和一个操作按钮或锁屏侧滑操作(alertAction)的标题。这些属性值是根据用户选择的当前语言本地化的字符串。如果你的通知也要在Apple Watch上显示,就需要给alertTitle赋值;
    • 设置applicationIconBadgeNumber属性显示应用角标数字;
    • 播放声音则需要设置soundName属性。你可以直接将一个在程序bundle中(或资源容器中)未本地化的自定义的声音赋给它,或者赋给一个系统默认的声音——UILocalNotificationDefaultSoundName。声音应该总是在显示提示消息或者角标的时候一起触发,而不应该单独出现。
  5. 你还可以选择在通过通知中的userInfo属性来添加自定义的数据。比如,当CloudKit记录改变时发送的通知中包含记录改变的标识,这样就可以在处理通知的时候通过该标识获取到发生改变的记录并更新该记录;

  6. 在iOS8之后,你也可以在本地通知中自定义操作,就像上面提到的那样做;

  7. 调度本地通知。

你可以调用方法scheduleLocalNotification:来安排你的本地通知。应用会在UILocalNotification对象指定的时间的那瞬间发送通知。当然,你也可以选择通过调用方法presentLocalNotificationNow:立即发送本地通知。

下面代码演示了上面的整个流程:

- (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];
}

你可以调用UIApplication对象的方法cancelLocalNotification:取消某个已经安排好的通知,也可以调用cancelAllLocalNotifications取消所有的通知。这些方法都可以手动清除当前所展示的通知提示。例如,你或许想要取消用户不再想要一个与提醒有关联的通知。

后台应用在某些消息、数据或者其他引起用户的兴趣东西到来的时候,就会发现本地通知是很有用的一个特性。在这种情况下,应用能通过UIApplication方法presentLocalNotificationNow:立即展示这些通知。(考虑到iOS只给后台应用一个有限时间运行的时候更是如此)。

在OS X系统中,下面提供了一个发送本地通知的示例。但是需要注意的是,如果你的应用大部分时间是在前台的,OS X并不会发送本地通知。当然,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];

处理本地和远程通知

回顾一下系统发送通知可能发生的情况:

1.当应用不在前台分发通知

这种情况下,系统会以消息提示、角标或者播放提示音来展示通知,也可能会显示一个或多个操作按钮供用户选择;

2.在iOS 8 通知中,用户点击自定义操作按钮

此情形下,iOS会调用application:handleActionWithIdentifier:forRemoteNotification:completionHandler:application:handleActionWithIdentifier:forLocalNotification:completionHandler:。不管哪种方法,你都能收到操作标识来决定哪个操作被点击了。你也可以收到不管是本地还是远程通知中需要处理该操作的所有信息。

3.用户点击了默认的操作按钮或者点击了应用图标

如果点击了默认的操作按钮(仅限于运行iOS的设备),系统会启动应用,并调用应用代理方法application:didFinishLaunchingWithOptions:,将通知的payload(对于远程通知)或本地通知对象(对于本地通知)传递给该方法。但最好不要在该方法中处理通知,在这里获取到的payload只是给你在真正处理通知之前可以进行一个更新的过程。

对于远程通知,系统将会调用应用代理的的application:didReceiveRemoteNotification:fetchCompletionHandler:方法;

如果在运行OS X的电脑中,点击应用程序图标就会调用方法applicationDidFinishLaunching:,这样就可以获取远程通知的payload。如果应用图标在运行iOS系统的设备上被点击,应用程序也会调用相同的方法,但是没有任何关于该通知的消息可以获得。

4.应用在前台是收到通知

在这种情况下,应用会调用应用代理方法application:didReceiveLocalNotification:或者application:didReceiveRemoteNotification:fetchCompletionHandler:方法(如果你没有实现该方法application:didReceiveRemoteNotification:fetchCompletionHandler:,iOS会调用application:didReceiveRemoteNotification:)。在OS X系统中,会调用application:didReceiveRemoteNotification:

应用会使用传递的远程通知的payload参数,或者在iOS系统中的UILocalNotification对象来帮助处理与该通知相关的上下文。理想的情况是,实现了代理中在每个系统下处理本地和远程通知所有可能情形下的处理方法。

  • 对于OS X,代理应该根据协议NSApplicationDelegate,实现代理方法application:didReceiveRemoteNotification:
  • 对于iOS,代理应该根据协议UIApplicationDelegate,实现application:didReceiveRemoteNotification:fetchCompletionHandler:或者application:didReceiveLocalNotification:。为了能够处理通知操作,需要实现application:handleActionWithIdentifier:forLocalNotification:completionHandler:或者application:handleActionWithIdentifier:forRemoteNotification:completionHandler:方法;

下面代码演示了在iOS应用中,实现代理方法application:didFinishLaunchingWithOptions:中如何处理本地通知。它首先从launch-options字典中根据UIApplicationLaunchOptionsLocalNotificationKey键获取了与UILocalNotification对象相关联的通知对象。然后根据UILocalNotification对象中的userInfo字典所携带的通知相关联的数据获取了产生to-do项通知的原因,初始化上下文。

- (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;
}

实现远程通知代理方法与此类似,我们希望你在每个平台上声明一个指定常量来做为键,从而利用该键来获取通知的payload。

  • 在iOS中,实现application:didFinishLaunchingWithOptions:方法,利用UIApplicationLaunchOptionsRemoteNotificationKey键从launch-options字典中获取payload
  • 在OS X中,实现applicationDidFinishLaunching:方法,利用NSApplicationLaunchUserNotificationKey键从NSNotification对象的userInfo字典获取payload字典。

payload本身是个字典对象,包含了通知的所有元素——提醒消息、角标、提示音等等。当然也可以包含自定义的数据用来为初始化用户界面提供上下文。参考The Remote Notification Payload获取更多关于远程通知payload的信息。

重要:远程通知的推送是得不到保障的,所以你不能使用通知payload来传递敏感数据或者那些不能通过其他方式获得的数据。

当在处理通知方法中处理远程通知时,应用代理可能会执行一些主要的附加工作,在应用启动后,代理应该连接到你的服务器并且获取服务器在等待发送的数据。

注:一个客户端应用应该异步或者在其他线程(相对于主线程),时刻与服务器保持沟通。

下面代码展示了应用程序在前台时,在代理方法application:didReceiveLocalNotification:中如何处理本地通知对象的过程,这和上面一段代码做的工作其实是一致的。

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

如果你希望应用程序在前台时,能捕获系统分发的远程通知,那么应用代理就必须实现application:didReceiveRemoteNotification:fetchCompletionHandler:方法。代理将会开始执行像下载数据、消息或者其他数据项的工作。结束后,应该移除掉应用角标。该方法的第二个参数就是通知payload,你可以利用payload中自定义的属性值来改变当前应用程序的上下文。

调度基于地理位置的本地通知

在iOS8之后,你可以创建基于地理位置的本地通知——当用户到达某一地理位置时推送通知。UILocalNotification对象能够配置一个Core Location区域对象。当用户进入了这个指定的区域,iOS就会推送通知。你可以在用户每次进出这个区域的边界的时候指定推送一次还是每次都进行推送。

注册基于地理位置的本地通知

基于地理位置的本地通知需要你的设置的应用支持Core Location。你必须配置好CLLocationManager对象,且需要提供其代理,最后你还要请求使用“位置”的授权。请求使用请求至少需要调用requestWhenInUseAuthorization方法,如下代码示例中展示的一样:

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 local notifications
[locMan requestWhenInUseAuthorization];

第一次请求“位置”服务授权时,iOS会询问用户“允许”或”拒绝“你的请求。当展示给用户时,iOS会显示应用Info.plist文件里NSLocationWhenInUseUsageDescription键对应的说明性文本。该键是强制的、必须提供的,如果你未提供该键,那么你的”位置“服务就无法启动。

即使你的应用在后台或者被挂起,用户也有可能看到基于位置的通知。然而,直到用户对提醒做出交互以及你的应用被允许使用用户的位置,你的应用才会收到回调。

处理Core Location回调

在启动的时候,你需要检查授权状态并且将该状态存储起来以决定你是需要允许或者不允许使用基于地理位置的本地通知。在Core Location 管理对象中第一个需要实现的代理回调就是locationManager:didChangeAuthorizationStatus:,,这个回调会报告当前授权状态的改变。首先,检查该回调传进来的参数状态是否为kCLAuthorizationStatusAuthorizedWhenInUse,假如是则意味着你的应用是被授权用于跟踪用户位置的。接着,你就可以调度基于位置的本地通知了。如下代码所示:

- (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
    }
}

下面代码展示了当用户在进入指定区域时如何调度通知的。首先你必须要做的就是像根据时间出发的本地通知一样创建一个UILocalNotification实例对象,定义它的类型,在该示例中,是个提醒框:

- (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];
}

当用户进入上面代码所指定的区域时,确保你的应用不在前台运行,应用就会显示一个提醒框,上面显示”You have arrived!“。上面指定的是该通知只会被触发一次,也就是第一次进入或者离开该区域。这也是默认上的设置,所以不必要讲其设置为YES,但假如那样对你的用户或者应用是有意义的话,你也可设置该属性为NO

下面一行,你创建了一个CLCircularRegion对象来赋值给UILocalNotification对象的区域属性。在这里,我们指定了一个应用级别的地理位置,即用户进入到一个以应用为中心,某一半径的圆的时候,就会触发通知。在这个例子中,我们使用了一个CLCircularRegion属性,你也可以用CLBeaconRegion或者其他任何继承CLRegion的子类。

最后,调用UIApplication的单例方法scheduleLocalNotification:,想其他本地通知一样将通知传给方法。

处理基于地理位置的本地通知

当用户进入上面代码所指定的区域时,确保你的应用不在前台运行,应用就会显示一个提醒框,上面显示”You have arrived!“。你的应用可以在应用代理回调方法application:didFinishLaunchingWithOptions:中处理这个本地通知。如果当用户进出该区域时,你的应用正在前台运行,应用代理就会回调方法application:didReceiveLocalNotification:

application:didFinishLaunchingWithOptions:application:didReceiveLocalNotification:方法中处理基于位置的本地通知的逻辑是非常相似的。两个方法都能提供通知对象,一个包含region属性的UILocalNotification实例对象。如果region属性不为nil,那么通知就是一个基于位置的通知,你就据此可以做任何对你的应用有意义的事情。下面示例代码调用了一个假设的方法tellFriendsUserArrivedAtRegion::

- (void)application:(UIApplication *)application
                         didReceiveLocalNotification: (UILocalNotification *)notification {

    CLRegion *region = notification.region;

    if (region) {
           [self tellFriendsUserArrivedAtRegion:region];
    }
}

最后,记住如果用户在”设置“应用中,”隐私“>”位置“下关闭了位置服务,那么application:didReceiveLocalNotification:就不会被调用。

预处理自定义提示音

对于iOS远程通知,你可以在显示本地货远程通知的时,为你的应用指定一个自定义的声音播放。这个音频文件可以在应用的main bundle里或者在应用的资源容器Library/Sounds文件夹下面。

自定义提示音是由iOS系统音频设备播放的,所以它唏嘘是下面一种音频数据格式:

  • Linear PCM
  • MA4 (IMA/ADPCM)
  • µLaw
  • aLaw

你可以将音频数据打包成aiffwavcaf文件。然后在Xcode中,添加音频文件到你的项目中,就像其他资源文件一样添加到应用bundle或资源容器的Library/Sounds文件夹下.

你可以使用afconvert工具来转换音频,比如,要转换一个16bit的线性PCM(16-bit linear PCM)系统声音文件Submarine.aiffIMA4格式CAF文件,在Terminal中输入:

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


你可以通过在QuickTime播放器打开音频文件,然后点击Movie菜单,选择Show Movie Inspector来查看音频的数据格式。

自定义的音频必须在30s以内。如果超出该限制,那么系统就会播放默认的提示音。

将当前语言设置传递给服务器(远程通知)

如果你的应用在客户端没有使用aps字典里的loc-keyloc-args来获取本地化的提示消息,服务器就需要将推送消息的payload中的提示消息进行本地化。为了达到这个要求,服务器就需要知道用户设备所选择的优先语言(用户可以在”设置”应用General>International>Langage中设置)。客户端需要将首选语言项的标识符发送给服务器。该标识符在IETF BCP 47语言标识中规范化的表述,如”en”或”fr“。

注:Internet Engineering Task Force Best Current Practices 47——因特尔工程任务组最佳实践

更多关于客户端消息本地化以及loc-keyloc-args属性,参见The Remote Notification Payload

下面代码演示了如何获取当前选择的语言以及如何将其与服务器沟通。在iOS中,NSLocale对象的preferredLanguages属性会返回一个数组,该数组里面一个对象:一个经过封装的首选语言代码标识符的字符串。UTF8String方法可以将该字符串对象转换为编码为UTF8的C字符串。

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

应用可能需要在每次用户改变当前时区设置的时候将首选语言项发送给服务器,为此,你需要监听名为NSCurrentLocaleDidChangeNotification的通知,然后在你的通知处理方法中,获取语言标识符,并发送给服务器。

如果语言标识符是你的应用所不支持的,那么服务器就应该将提示文本本地化为一个广泛使用的语言比如英语和西班牙语。