iOS 10 消息推送(UserNotifications)秘籍总结(二)

背景

上一篇博客 iOS 10 消息推送(UserNotifications)秘籍总结(一) 发布后被 简书编辑推荐至首页 ,这着实让我受宠若惊啊。可是好事不长,后面发生了让我伤心欲绝的事,我的女朋友不要我了%>_<%。刚刚放完国庆假期,你们还沉浸在国庆的喜悦中没回过神来,而我则迷失了前进的方向不能自拔,没有了寄托和疼爱的那个人了!

爱情中最遗憾的事大概就是如此吧
我们曾爱的人到撕心裂肺,但时时刻刻都在互相伤害,谁也不懂退让,也不会给对方宽容,相爱相杀演绎到了极致而分手,因为那时我们相爱太早了,浑身带刺,根本不能给对方想要的生活方式,分道扬镳时是一种成全,更是一种解脱。但是多年的感情放手真的那么容易吗?我相信地球是圆的,再经过了多次辗转之后再次重逢,那时候的我们会带着打磨好的自己彼此欣赏,不会再为了谁洗碗这样的小事而争吵,不会再用言语伤害最爱的人!

我希望有个如你一般的人。如这山间清晨一般明亮清爽的人,如奔赴古城道路上阳光一般的人,温暖而不炙热,覆盖我所有肌肤。由起点到夜晚,由山野到书房,一切问题的答案都很简单。我希望有个如你一般的人,贯彻未来,数遍生命的公路牌。只要最后是你,就好

这篇博客是根据上一篇博客代码 iOS 10 消息推送(UserNotifications)秘籍总结(一) 继续编写的,后面我会把Demo地址发出来供大家学习测试!
本篇代码较多,请做好心理准备,如果看晕,本楼概不负责!

Notification Actions

早在iOS8和iOS9下,notification增加了一些新的特性:
iOS 8增加了下拉时的Action按钮,像微信一样;
iOS 9增加了像信息一样的可以下拉直接输入;

iOS 10 中,可以允许推送添加交互操作 action,这些 action 可以使得 App 在前台或后台执行一些逻辑代码。如:推出键盘进行快捷回复,该功能以往只在 iMessage 中可行。

在 iOS 10 中,这叫 category,是对推送功能的一个拓展,可以通过 3D-Touch 触发,如果你的你的手机不支持3D-Touch也没关系,右滑则会出现view和clear选项来触发。

1、创建Action

    UNNotificationAction *lookAction = [UNNotificationAction actionWithIdentifier:@"action.join" title:@"接收邀请" options:UNNotificationActionOptionAuthenticationRequired];
    UNNotificationAction *joinAction = [UNNotificationAction actionWithIdentifier:@"action.look" title:@"查看邀请" options:UNNotificationActionOptionForeground];
    UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@"action.cancel" title:@"取消" options:UNNotificationActionOptionDestructive];
    UNTextInputNotificationAction *inputAction = [UNTextInputNotificationAction actionWithIdentifier:@"action.input" title:@"输入" options:UNNotificationActionOptionForeground textInputButtonTitle:@"发送" textInputPlaceholder:@"tell me loudly"];
1. UNNotificationActionOptions是一个枚举类型,是用来标识Action触发的行为方式分别是:
需要解锁显示,点击不会进app。
UNNotificationActionOptionAuthenticationRequired = (1 << 0),
红色文字。点击不会进app。    
UNNotificationActionOptionDestructive = (1 << 1),
黑色文字。点击会进app。    
UNNotificationActionOptionForeground = (1 << 2),
2. UNNotificationAction是按钮action,UNTextInputNotificationAction是输入框Action
3. 创建 UNTextInputNotificationAction 比 UNNotificationAction 多了两个参数
 buttonTitle 输入框右边的按钮标题
 placeholder 输入框占位符

2、 创建category

 UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@"Dely_locationCategory" actions:@[lookAction, joinAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
+ (instancetype)categoryWithIdentifier:(NSString *)identifier actions:(NSArray<UNNotificationAction *> *)actions intentIdentifiers:(NSArray<NSString *> *)intentIdentifiers options:(UNNotificationCategoryOptions)options;
identifier 标识符是这个category的唯一标识,用来区分多个category,
这个id不管是Local Notification,还是remote Notification,一定要有并且要保持一致 ,切记切记!下面注意看截图
actions 是你创建action的操作数组
intentIdentifiers 意图标识符 可在 <Intents/INIntentIdentifiers.h> 中查看,主要是针对电话、carplay 等开放的 API
options 通知选项 枚举类型 也是为了支持 carplay

3、把category添加到通知中心

 // 将 category 添加到通知中心
 UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
 [center setNotificationCategories:[NSSet setWithObject:notificationCategory]];

4、完整Demo例子

  • 本地通知Local Notification
  • "title" : "iOS远程消息,我是主标题!-title", "subtitle" : "iOS远程消息,我是主标题!-Subtitle", "body" : "Dely,why am i so handsome -body" "category" : "Dely_locationCategory", "badge" : "2"

    下面就是创建按钮Action的完整代码

    + (void)addNotificationAction{
        //创建按钮Action
        UNNotificationAction *lookAction = [UNNotificationAction actionWithIdentifier:@"action.join" title:@"接收邀请" options:UNNotificationActionOptionAuthenticationRequired];
        UNNotificationAction *joinAction = [UNNotificationAction actionWithIdentifier:@"action.look" title:@"查看邀请" options:UNNotificationActionOptionForeground];
        UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@"action.cancel" title:@"取消" options:UNNotificationActionOptionDestructive];
        // 注册 category
        // * identifier 标识符
        // * actions 操作数组
        // * intentIdentifiers 意图标识符 可在 <Intents/INIntentIdentifiers.h> 中查看,主要是针对电话、carplay 等开放的 API。
        // * options 通知选项 枚举类型 也是为了支持 carplay
        UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@"Dely_locationCategory" actions:@[lookAction, joinAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
        // 将 category 添加到通知中心
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        [center setNotificationCategories:[NSSet setWithObject:notificationCategory]];
    

    收到消息如下:

    + (void)addNotificationAction2{
        // 创建 UNTextInputNotificationAction 比 UNNotificationAction 多了两个参数
        // * buttonTitle 输入框右边的按钮标题
        // * placeholder 输入框占位符
        UNTextInputNotificationAction *inputAction = [UNTextInputNotificationAction actionWithIdentifier:@"action.input" title:@"输入" options:UNNotificationActionOptionForeground textInputButtonTitle:@"发送" textInputPlaceholder:@"tell me loudly"];
        // 注册 category
        UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@"Dely_locationCategory" actions:@[inputAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        [center setNotificationCategories:[NSSet setWithObject:notificationCategory]];
    

    收到消息如下:

    5、事件的操作

    现在我们能收到消息了,你以为就结束了嘛。错!因为我们要操作这个消息的,如果只是做到这里就结束了话,那我点击那个按钮都不知道,或者我输入什么文字也不知道,那要这个功能何用,那老板会对你说到财务领工资吧,明天别来了!我们所有的学习都是为了更好为老板挣钱的不是嘛!这就是我们程序猿的价值啊!需要我们做获取操作事件,那就继续往下看:

    我上一篇博客说过所有的push(不管远端或者本地)点击都会走到下面的代理方法

    - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __TVOS_PROHIBITED;
    

    那我们点击某一个按钮或者输入什么文字肯定也在这里操作了:

    //App通知的点击事件
    - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{
        //点击或输入action
        NSString* actionIdentifierStr = response.actionIdentifier;
        if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
            NSString* userSayStr = [(UNTextInputNotificationResponse *)response userText];
            NSLog(@"actionid = %@\n  userSayStr = %@",actionIdentifierStr, userSayStr);
            //此处省略一万行需求代码。。。。
        if ([actionIdentifierStr isEqualToString:@"action.join"]) {
            //此处省略一万行需求代码
             NSLog(@"actionid = %@\n",actionIdentifierStr);
        }else if ([actionIdentifierStr isEqualToString:@"action.look"]){
            //此处省略一万行需求代码
            NSLog(@"actionid = %@\n",actionIdentifierStr);
       //下面代码就不放进来了,具体看Demo
    

    小结:上面介绍了category,到这里功能才算完整。IOS 10的category其实是独立出来的不要和创建push混为一谈,它只是一个扩展功能,可加可不加的!

    Media Attachments和自定义推送界面

    本地推送和远程推送同时都可支持附带Media Attachments。不过远程通知需要实现通知服务扩展UNNotificationServiceExtension,在service extension里面去下载attachment,但是需要注意,service extension会限制下载的时间(30s),并且下载的文件大小也会同样被限制。这里毕竟是一个推送,而不是把所有的内容都推送给用户。所以你应该去推送一些缩小比例之后的版本。比如图片,推送里面附带缩略图,当用户打开app之后,再去下载完整的高清图。视频就附带视频的关键帧或者开头的几秒,当用户打开app之后再去下载完整视频。

    attachment支持图片,音频,视频,附件支持的类型及大小

    附件类型和大小.png

    系统会在通知注册前校验附件,如果附件出问题,通知注册失败;校验成功后,附件会转入attachment data store;如果附件是在app bundle,则是会被copy来取代move
    media attachments可以利用3d touch进行预览和操作
    attachment data store的位置?利用代码测试 获取在磁盘上的图片文件作为attachment,会发现注册完通知后,图片文件被移除,在app的沙盒中找不到该文件在哪里; 想要获取已存在的附件内容,文档中提及可以通过UNUserNotificationCenter中方法,但目前文档中这2个方法还是灰的,见苹果开发者文档

    //就是这两个方法
    getDataForAttachment:withCompletionHandler:
    getReadFileHandleForAttachment:withCompletionHandler:
    

    1、准备工作
    附件限定https协议,所以我们现在找一个支持https的图床用来测试,我之前能测试的图床现在不能用了。你们可以自行googole,这是我之前上传的图片链接:https://p1.bpimg.com/524586/475bc82ff016054ds.jpg
    具体附件格式可以查看苹果开发文档

    2、添加新的Targe--> Notification Service
    先在Xcode 打开你的工程,File-->New-->Targe然后添加这个Notification Service:

    Notification Service.png

    然后会自动创建一个 UNNotificationServiceExtension 的子类 NotificationService,通过完善这个子类,来实现你的需求。

    点开 NotificationService.m 会看到 2 个方法:

    // Call contentHandler with the modified notification content to deliver. If the handler is not called before the service's time expires then the unmodified notification will be delivered.
    // You are expected to override this method to implement push notification modification.
    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *contentToDeliver))contentHandler;
    // Will be called just before this extension is terminated by the system. You may choose whether to override this method.
    - (void)serviceExtensionTimeWillExpire;
    

    didReceiveNotificationRequest让你可以在后台处理接收到的推送,传递最终的内容给 contentHandler
    serviceExtensionTimeWillExpire 在你获得的一小段运行代码的时间即将结束的时候,如果仍然没有成功的传入内容,会走到这个方法,可以在这里传肯定不会出错的内容,或者他会默认传递原始的推送内容

    主要的思路就是在这里把附件下载下来,然后才能展示渲染,下面是下载保存的相关方法:

    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        self.contentHandler = contentHandler;
        self.bestAttemptContent = [request.content mutableCopy];
        NSString * attchUrl = [request.content.userInfo objectForKey:@"image"];
        //下载图片,放到本地
        UIImage * imageFromUrl = [self getImageFromURL:attchUrl];
        //获取documents目录
        NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString * documentsDirectoryPath = [paths firstObject];
        NSString * localPath = [self saveImage:imageFromUrl withFileName:@"MyImage" ofType:@"png" inDirectory:documentsDirectoryPath];
        if (localPath && ![localPath isEqualToString:@""]) {
            UNNotificationAttachment * attachment = [UNNotificationAttachment attachmentWithIdentifier:@"photo" URL:[NSURL URLWithString:[@"file://" stringByAppendingString:localPath]] options:nil error:nil];
            if (attachment) {
                self.bestAttemptContent.attachments = @[attachment];
        self.contentHandler(self.bestAttemptContent);
    - (UIImage *) getImageFromURL:(NSString *)fileURL {
        NSLog(@"执行图片下载函数");
        UIImage * result;
        //dataWithContentsOfURL方法需要https连接
        NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fileURL]];
        result = [UIImage imageWithData:data];
        return result;
    //将所下载的图片保存到本地
    - (NSString *) saveImage:(UIImage *)image withFileName:(NSString *)imageName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath {
        NSString *urlStr = @"";
        if ([[extension lowercaseString] isEqualToString:@"png"]){
            urlStr = [directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"png"]];
            [UIImagePNGRepresentation(image) writeToFile:urlStr options:NSAtomicWrite error:nil];
        } else if ([[extension lowercaseString] isEqualToString:@"jpg"] ||
                   [[extension lowercaseString] isEqualToString:@"jpeg"]){
            urlStr = [directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"jpg"]];
            [UIImageJPEGRepresentation(image, 1.0) writeToFile:urlStr options:NSAtomicWrite error:nil];
        } else{
            NSLog(@"extension error");
        return urlStr;
    - (void)serviceExtensionTimeWillExpire {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        self.contentHandler(self.bestAttemptContent);
    

    apes如下:

    "aps":{ "alert" : { "title" : "iOS远程消息,我是主标题!-title", "subtitle" : "iOS远程消息,我是主标题!-Subtitle", "body" : "Dely,why am i so handsome -body" "sound" : "default", "badge" : "1", "mutable-content" : "1", "category" : "Dely_category" "image" : "https://p1.bpimg.com/524586/475bc82ff016054ds.jpg", "type" : "scene", "id" : "1007"
  • Notification Service Extension在使用时需要配置相关证书
  • mutable-content这个键值为1,这意味着此条推送可以被 Service Extension 进行更改,也就是说要用Service Extension需要加上这个键值为1.
  • 特别说明:
    有很多朋友经常会问我加断点调试怎么不走相应方法,我这里统一回复下。你要选择相应的target来运行工程:

    补充说明:
    在最新的Xcode9.2中,断点也是不会走的,一个朋友找了很长时间的原因发现是xcode的问题,在xcode8.0上就可以跑到断点。希望苹果尽快修复这个bug,这个朋友的学习热情很值得我们学习。

    3、添加新的Targe--> Notification Content

    先在Xcode 打开你的工程,File-->New-->Targe然后添加这个 Notification Content:

    点开 NotificationViewController.m 会看到 2 个方法:

    - (void)viewDidLoad;
    - (void)didReceiveNotification:(UNNotification *)notification;
    

    前者渲染UI,后者获取通知信息,更新UI控件中的数据。

    在MainInterface.storyboard中自定你的UI页面,可以随意发挥,但是这个UI见面只能用于展示,并不能响应点击或者手势其他事件,只能通过category来实现,下面自己添加view和约束

    然后把view拉到.m文件中,代码如下:

    #import "NotificationViewController.h"
    #import <UserNotifications/UserNotifications.h>
    #import <UserNotificationsUI/UserNotificationsUI.h>
    @interface NotificationViewController () <UNNotificationContentExtension>
    @property IBOutlet UILabel *label;
    @property (weak, nonatomic) IBOutlet UIImageView *imageView;
    @implementation NotificationViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any required interface initialization here.
    //    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    //    [self.view addSubview:view];
    //    view.backgroundColor = [UIColor redColor];
    - (void)didReceiveNotification:(UNNotification *)notification {
        self.label.text = notification.request.content.body;
        UNNotificationContent * content = notification.request.content;
        UNNotificationAttachment * attachment = content.attachments.firstObject;
        if (attachment.URL.startAccessingSecurityScopedResource) {
            self.imageView.image = [UIImage imageWithContentsOfFile:attachment.URL.path];
    

    有人要有疑问了,可不可以不用storyboard来自定义界面?当然可以了!
    只需要在Notifications Content 的info.plist中把NSExtensionMainStoryboard替换为NSExtensionPrincipalClass,并且value对应你的类名!
    然后在viewDidLoad里用纯代码布局就可以了

    上面介绍了远端需要Service Extension 的远端推送
    iOS 10附件通知(图片、gif、音频、视频)。不过对图片和视频的大小做了一些限制(图片不能超过 10M,视频不能超过 50M),而且附件资源必须存在本地,如果是远程推送的网络资源需要提前下载到本地。
    如果是本地的就简单了只需要在Service Extension的NotificationService.m的- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler拿到资源添加到Notification Content,在Notification Content的控制器取到资源自己来做需求处理和展示

    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        self.contentHandler = contentHandler;
        self.bestAttemptContent = [request.content mutableCopy];
        // 资源路径
        NSURL *videoURL = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];
        // 创建附件资源
        // * identifier 资源标识符
        // * URL 资源路径
        // * options 资源可选操作 比如隐藏缩略图之类的
        // * error 异常处理
        UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"video.attachment" URL:videoURL options:nil error:nil];
        // 将附件资源添加到 UNMutableNotificationContent 中
        if (attachment) {
            self.bestAttemptContent.attachments = @[attachment];
        self.contentHandler(self.bestAttemptContent);