理解iOS的用户通知(下篇)

达芬奇密码2018-06-22 13:37

开启远程通知功能

注册通知类型

  • 对于 iOS7,无需此步骤;
  • 对于 iOS8 及以后的系统,若需要使用远程通知功能,则需要注册通知类型。步骤与 "本地通知的使用" 中 "开启本地通知功能" 是完全相同的,此处不再重复。

注册远程通知

基本流程为:

  1. 注册通知类型,上一小节已经做了介绍;
  2. 使用 registerForRemoteNotifications 注册远程通知(对于 iOS7 使用 registerForRemoteNotificationTypes:);
  3. 使用 application:didRegisterForRemoteNotificationsWithDeviceToken: 接收 APNs 返回的 Token, 使用 application:didFailToRegisterForRemoteNotificationsWithError: 处理注册错误;
  4. 如果上一步骤中注册成功了,那么将得到的 Token 发送给 Provider。

注意:

  1. 目前看来,对于 iOS9,每次重新安装应用后得到的 Token 是不一样的,而且每次重装系统也会改变,所以 每次应用启动时都需要按上面的步骤注册一次
  2. 不要将之前的 Token 缓存,当需要将 Token 传送到 Provider 时,一定要使用 registerForRemoteNotifications 获取,并使用回调处理注册结果; 当应用注册过通知,而且 Token 没有改变时,系统会立即返回结果,不会去 APNs 请求。 这里猜测系统帮助将 Token 缓存下来,且与应用的状态进行了关联,如果应用当前状态没有改变,那么会立即将系统存下的 Token 返回。 为了证明这点,可以将网络关闭进行测试,如果 App 没有卸载,也是可以获取到 Token 的;
  3. 一定要有开启了 Push 功能的证书,才能正常使用远程推送。

注册远程通知的示例代码如下:

- (void)registerRemoteNotifications {
    // 区分是否是 iOS8 or later
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerForRemoteNotifications)]) {
        // 这里 types 可以自定义,如果 types 为 0,那么所有的用户通知均会静默的接收,系统不会给用户任何提示(当然,App 可以自己处理并给出提示)
        UIUserNotificationType types = (UIUserNotificationType) (UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert);
        // 这里 categories 可暂不深入,本文后面会详细讲解。
        UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        // 当应用安装后第一次调用该方法时,系统会弹窗提示用户是否允许接收通知
        [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
    } else {
        UIRemoteNotificationType types = UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound;
        [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types];
    }
}

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings {
    // Register for remote notifications.
    [[UIApplication sharedApplication] registerForRemoteNotifications];
}

// Handle register result.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    //获得 device token,这一步处理为字符串的操作很重要
    NSString *token = [[[deviceToken description]
                        stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]
                        stringByReplacingOccurrencesOfString:@" "
                        withString:@""];
    NSLog(@"DeviceToken string, %@", token);
    [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
    // 将 token 发送给 Provider
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSLog(@"Error in registration for apns service. Error: %@", error);
}

发送远程通知

远程通知的内容

Provider 发送给 APNs 服务器的内容格式如下:

{
   // aps key 是必须要有的
   "aps" : {
      "alert" : {
         "title" : "通知的概要,对 8.2 以前的系统本选项无效"
         "body" : "通知的具体内容",
         "loc-key" : "GAME_PLAY_REQUEST_FORMAT",
         "loc-args" : [ "Jenna", "Frank"]
      },
      "badge" : 3, // 角标数值
      "sound" : “chime.aiff" // 可以自定义提示音
   },

   "userName" : "username

上面只是简要介绍了常见的内容,如需要更加深度定制推送通知,建议查看: 苹果官方payload文档

远程通知的本地化处理

有两种方式:

  • 在 Provider 端进行本地化 App 可以将当前使用的语言发送给 Provider,Provider 在发送远程通知前,检查当前设备使用的语言,并做好本地化后发送给 APNs 服务器。App 发送当前使用的语言给 Provider 的示例代码:

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

一般来说,将当前系统语言信息发送给 Provider 时,也会将 Token 一起发送,这样 Provider 才能够在发送远程通知时根据不同目的设备进行本地化处理。 此外,当应用启动后,用户可能会修改系统语言,这时,App 需要监听 NSCurrentLocaleDidChangeNotification 通知,并在处理通知的方法中重新向 Provider 发送当前使用的语言。

  • 在客户端本地化 这种模式下,Provider 在发送远程通知时,需要设置 Payload -> alert 中的本地化相关属性,如下:

    {
         // aps key 是必须要有的
           "aps" : {
              "alert" : {
                 "title" : "通知的概要,对 8.2 以前的系统本选项无效",
                 "loc-key" : "Remote Notification",
                 "loc-args" : [ "hello", "world"]
              },
              "badge" : 3, // 角标数值
              "sound" : “chime.aiff" // 可以自定义提示音
           }
    }
    


上面 loc-key 以及 loc-args 就是本地化相关的属性,用于本地化 alert 中的 body。 当 App 收到此消息时,会根据系统当前的语言设置去相应的本地化文件中查找与 loc-key 对应的 value,如果 loc-key 对应的 value 是一个格式化的字符串,那么可以用 loc-args 传递参数。

假设本地化文件中: "Remote Notification" = "我们程序员通常钟爱:%@ %@" ,那么提示信息就是: "我们程序员钟爱:hello world"; 此外在本地化文件中我们也可以用 %n@"RemoteNotification"="@ %1$@",那么提示信息是:"我们程序员钟爱:world hello"。

同样的,title-loc-key 和 title-loc-args 是对 alert 中的 title 做本地化的。

处理收到的远程通知

这里分三种情况讨论如何处理远程通知:

应用处于前台

应用处于前台时,本地通知到达时,不会有提示音、通知栏横幅提示。但是 App 桌面图标的右上角角标是有数值显示的,所以即使在前台,我们也应该对角标数量做处理; 此时,我们可以在 application:didReceiveRemoteNotification:fetchCompletionHandler: 方法中获取到远程通知,示例代码如下:

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler {
    NSData *infoData = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:nil];
    NSString *info = [[NSString alloc] initWithData:infoData encoding:NSUTF8StringEncoding];
    [self.windowRootController displayNotification:[NSString stringWithFormat:@"From didReceiveRemoteNotification: %@", info]];
    // 这里将角标数量减一,注意系统不会帮助我们处理角标数量
    application.applicationIconBadgeNumber = notification.applicationIconBadgeNumber - 1;
}

应用处于后台

当应用处于后台时,远程通知到达时,会根据注册通知是设置的通知类型以及用户设置的通知类型进行提示,例如锁屏界面通知、通知栏通知、声音、角标。 此时如果滑动锁屏界面通知或点击通知栏通知,则会切换应用到前台,我们可以使用与应用处于前台时相同的获取通知的方式。 但是如果我们点击 App 桌面图标,则无法获取到用户通知,此时通知栏消息仍然会存在。此外,角标也不会变化,如果希望修改角标,则需要 App 进入前台后将其修改。

应用没有运行

这里有两种处理方式:

  • 与本地通知的处理方式相同,在 application:didFinishLaunchingWithOptions: 的 LaunchOptions 中获取通知,不过内部代码会略有不同,示例如下:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        NSDictionary *remoteNotif = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
        if (remoteNotif) {
            NSData *infoData = [NSJSONSerialization dataWithJSONObject:remoteNotif options:0 error:nil];
            NSString *info = [[NSString alloc] initWithData:infoData encoding:NSUTF8StringEncoding];
            [self.windowRootController displayNotification:[NSString stringWithFormat:@"From didFinishLaunch: %@", info]];
            [UIApplication sharedApplication].applicationIconBadgeNumber -= 1;
        }
        // Your own other codes.
        return YES;
    }
    
  • 与应用处于前后台时处理方式相同,使用 application:didReceiveRemoteNotification:fetchCompletionHandler: 方法,示例代码见 "应用处于前台" 时的处理。 对于远程通知,推荐使用此种方式处理。

    此外,对于远程通知,如果我们点击 App 桌面图标,则无法获取到用户通知,此时通知栏消息仍然会存在。此外,角标也不会变化,如果希望修改角标,则需要 App 进入前台后将其修改。

远程通知-静默推送

静默推送是指应用在前台或后台状态下,收到远程通知时,没有弹窗或横幅提示,即使处于后台也可以处理远程通知。具体使用流程如下:

  1. 打开应用工程 Target 的 Capacities,将 Background Modes 选项打开,并且勾选 Remote Notifications;
  2. 在 Provider 发送远程通知时,需要将远程通知 Payload 中的 aps 内的 content-available 设置为 1,如下:

    aps {  
         content-available: 1
         alert: {...}
    }
    
  3. 应用需要实现 application:didReceiveRemoteNotification:fetchCompletionHandler: 方法接收静默推送。

有几点需要注意:

  1. 使用静默推送时,alert 字段不应有任何信息,但可以设置 aps 内的自定义字段;
  2. sound 和 badge 字段可以设置,但最好不设置,否则会有提示音;
  3. 静默推送只有当应用处于前台或后台时才能处理,当应用没有启动时是收不到静默推送的;
  4. 处理静默推送时,不能做耗时操作,因为系统只为这种处理行为分配少量时间,如下载文件之类的操作请使用后台下载服务。

可操作通知

首先需要注意的是,可操作通知只适用于 iOS8 及以后的系统。

可操作通知其实并不是一种新的通知形式,它只是在这本地通知和远程通知的基础上加了一些可操作的行为而已。为了直观说明什么是可操作通知,可以参考下图:


可操作通知为用户提供了在通知提示中方便执行操作的方式,在使用横幅提示通知消息时,最多可以有两个操作,在使用弹窗提示通知消息是,最多可以有四个操作。下面讲解如何使用:

定义可操作通知的行为

基本使用方法:

  1. 创建一个 UIMutableUserNotificationAction 对象,并按需求配置该对象的属性,示例代码:

    UIMutableUserNotificationAction *acceptAction = [[UIMutableUserNotificationAction alloc] init];
    // 为该操作设置一个 id
    acceptAction.identifier = @"accept";
    // 设置该操作对应的 button 显示的字符串
    acceptAction.title = @"Accept";
    // 指定是否需要应用处于运行状态
    acceptAction.activationMode = UIUserNotificationActivationModeBackground;
    // 表示该操作是否有害,若设置为 YES,则对应的button会有高亮
    acceptAction.destructive = NO;
    // 当锁屏时收到可操作通知,该属性表示是否必须解锁才能执行该操作
    acceptAction.authenticationRequired = YES;
    
  2. 创建一个 UIMutableUserNotificationCategory 对象,并将自定义的操作通过 setActions: 的方式设置给 category 对象。代码如下:

    // 这里为了测试,又新建了两个 action,declineAction 和 maybeAction ,代码可见 demo
    UIMutableUserNotificationCategory *inviteCategory = [[UIMutableUserNotificationCategory alloc] init];
    // 设置一个 ID,用于本地通知或远程通知时指定该通知可执行的操作group
    inviteCategory.identifier = @"Action";
    // 为弹窗模式设置 actions
    [inviteCategory setActions:@[acceptAction, maybeAction, declineAction] forContext:UIUserNotificationActionContextDefault];
    // 为横幅模式设置 actions
    [inviteCategory setActions:@[acceptAction, declineAction] forContext:UIUserNotificationActionContextMinimal];
    
  3. 注册通知类型以及可操作的actions

    类似于本地通知和远程通知,调用 registerUserNotificationSettings: 注册通知,只是这里的 setting 加入了我们上面定义的 category。

    NSSet *categories = [NSSet setWithObjects:inviteCategory, nil];
    // 这里 types 可以自定义,如果 types 为 0,那么所有的用户通知均会静默的接收,系统不会给用户任何提示(当然,App 可以自己处理并给出提示)
    UIUserNotificationType types = (UIUserNotificationType) (UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert);
    // 这里 categories 可暂不深入,本文后面会详细讲解。
    UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:categories];
    // 当应用安装后第一次调用该方法时,系统会弹窗提示用户是否允许接收通知
    [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];

如果将可执行通知用于远程通知,那么需要按照远程通知的注册方式获取 token,可参考远程通知的注册。

发送可操作通知

之前说过,可操作通知只是在本地通知和远程通知的基础上加了自定义的操作,所以发送可操作通知就是发送本地通知或远程通知。 不过,如果希望我们自定义的 action 有效,在发送本地通知或远程通知时需要进行一些改变:

  • 本地通知的可操作通知 为 UILocalNotification 对象设置我们自定义的 category。如下:

    UILocalNotification *notification = [[UILocalNotification alloc] init]; // Other configurations notification.category = @"Action"; [[UIApplication sharedApplication] scheduleLocalNotification:notification];

  • 远程通知的可操作通知 在远程通知的 Payload 中设置我们自定义的 category,如下:

    {

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

    }

处理可操作通知

处理可操作通知与处理本地通知和远程通知相同,唯一的不同点就是当用户执行了某个操作后, 应用可以在后台运行 application:handleActionWithIdentifier:forRemoteNotification:completionHandler: 处理通知(例如后台更新数据等操作),我们可以在这个回调里快速的执行操作:

- (void)application:(UIApplication *) application
              handleActionWithIdentifier: (NSString *) identifier
          // either forLocalNotification: (NSDictionary *) notification or
                   forRemoteNotification: (NSDictionary *) notification
                       completionHandler: (void (^)()) completionHandler {

    if ([identifier isEqualToString: @"accept"]) {
        [self handleAcceptActionWithNotification:notification];
    }

    // 执行自定义代码完成后必须调用
    completionHandler();
}

对于本地通知我们可以使用 application:handleActionWithIdentifier:forLocalNotification:completionHandler:

可操作通知到底有什么好处?

这里举个例子说明: 假如A向B发出了一个出席发布会邀请,并且 App 是以远程通知的方式接收到该信息,那么当不使用可操作通知的时候,我们需要做的事情主要包括:

  1. 用户需要打开应用;
  2. App 查看远程通知的内容是一个邀请,那么 App 应该弹窗提示用户是否接受该邀请;
  3. 用户选择后,App 通过 Http(也可以使用其他通信协议) 将结果返回给服务器;
  4. 邀请通知处理完毕。

那么,如果我们使用可操作通知,可以很简单的做到这件事情:

  1. 用户选择接受或拒绝邀请(用户无需打开 App);
  2. App 通过可操作通知的回调处理用户操作结果,将结果发送给服务器;
  3. 邀请通知处理完毕。

可以看到,不论是从用户角度还是开发者角度,可操作通知都极大的方便了处理具有可操作动作的这类通知。


总结

到这里已经讲解完成了用户通知的内容,文章包含了苹果给出的用户通知的基本所有用法,如果想要确认文档是否有误可自行参考 Local and Remote Notification Programming Guide , 此外,如果本文中内容有误,欢迎指出与讨论。

本文来自网易实践者社区,经作者白天宇授权发布。

相关阅读:理解iOS的用户通知(上篇)